diff --git a/.bazelrc b/.bazelrc
index 53cb710dc1..b083e61ee8 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -22,3 +22,9 @@ build --spawn_strategy=standalone
build --strategy=SwiftCompile=standalone
build --define RULES_SWIFT_BUILD_DUMMY_WORKER=1
+common:index_build --experimental_convenience_symlinks=ignore
+common:index_build --bes_backend= --bes_results_url=
+common:index_build --nolegacy_important_outputs
+common:index_build --show_result=0
+common:index_build --define=buildNumber=10000
+common:index_build --define=telegramVersion=12.2.2
diff --git a/.gitignore b/.gitignore
index 4a8af5793f..0e855890d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,3 +74,4 @@ buildServer.json
Telegram.LSP.json
**/.build/**
spm-files
+.bsp/**
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index e66c7387a0..9e7292b4dc 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -35,3 +35,6 @@ url=../tgcalls.git
[submodule "third-party/XcodeGen"]
path = third-party/XcodeGen
url = https://github.com/yonaskolb/XcodeGen.git
+[submodule "build-system/bazel-rules/sourcekit-bazel-bsp"]
+ path = build-system/bazel-rules/sourcekit-bazel-bsp
+url=https://github.com/ali-fareed/sourcekit-bazel-bsp.git
diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json
new file mode 100644
index 0000000000..7715da74d8
--- /dev/null
+++ b/.sourcekit-lsp/config.json
@@ -0,0 +1,9 @@
+{
+ "backgroundIndexing": true,
+ "backgroundPreparationMode": "build",
+ "defaultWorkspaceType": "buildServer",
+ "logging": {
+ "level": "error",
+ "privacyLevel": "sensitive"
+ }
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9c36c383b3..8ca4257fe3 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,44 +1,31 @@
{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
"configurations": [
{
- "type": "swift",
- "request": "launch",
- "args": [],
- "cwd": "${workspaceFolder:telegram-ios}",
"name": "Debug Telegram",
- "program": "${workspaceFolder:telegram-ios}/.build/debug/Telegram",
- "preLaunchTask": "swift: Build Debug Telegram"
+ "type": "lldb-dap",
+ "request": "attach",
+ "preLaunchTask": "_launch_telegram",
+ "debuggerRoot": "${workspaceFolder}",
+ "attachCommands": [
+ "process connect connect://localhost:6667"
+ ],
+ "internalConsoleOptions": "openOnSessionStart",
+ "timeout": 1000
},
{
- "type": "swift",
- "request": "launch",
- "args": [],
- "cwd": "${workspaceFolder:telegram-ios}",
- "name": "Release Telegram",
- "program": "${workspaceFolder:telegram-ios}/.build/release/Telegram",
- "preLaunchTask": "swift: Build Release Telegram"
+ "name": "Restart Debug Telegram",
+ "type": "lldb-dap",
+ "request": "attach",
+ "preLaunchTask": "Restart Telegram Debug",
+ "debuggerRoot": "${workspaceFolder}",
+ "attachCommands": [
+ "process connect connect://localhost:6667"
+ ],
+ "internalConsoleOptions": "openOnSessionStart",
+ "timeout": 1000
},
- {
- "type": "swift",
- "request": "launch",
- "args": [],
- "cwd": "${workspaceFolder:telegram-ios}",
- "name": "Debug telegram-ios",
- "program": "${workspaceFolder:telegram-ios}/.build/debug/telegram-ios",
- "preLaunchTask": "swift: Build Debug telegram-ios"
- },
- {
- "type": "swift",
- "request": "launch",
- "args": [],
- "cwd": "${workspaceFolder:telegram-ios}",
- "name": "Release telegram-ios",
- "program": "${workspaceFolder:telegram-ios}/.build/release/telegram-ios",
- "preLaunchTask": "swift: Build Release telegram-ios"
- }
- ]
+ ],
+ "options": {
+ "console": "integratedTerminal",
+ }
}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3090f4c8be..a949672dbf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,4 @@
{
- "sweetpad.build.xcodeWorkspacePath": "Telegram/Telegram.xcodeproj/project.xcworkspace",
- "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
- "lldb.launch.expressions": "native",
"search.followSymlinks": false,
"files.exclude": {
".git/**": true
@@ -13,5 +10,17 @@
".git/**": true
},
"files.associations": {
- }
+ },
+ "swift.sourcekit-lsp.backgroundIndexing": "on",
+ "swift.sourcekit-lsp.trace.server": "messages",
+ "terminal.integrated.profiles.osx": {
+ "zsh": {
+ "path": "/bin/zsh",
+ "args": [
+ "-l",
+ "-i"
+ ]
+ }
+ },
+ "swift.sourcekit-lsp.serverPath": "${workspaceFolder}/build-input/sourcekit-lsp",
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 79b33fc863..efabdea411 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,6 +1,101 @@
{
- "version": "2.0.0",
- "tasks": [
- ]
- }
-
\ No newline at end of file
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build Telegram",
+ "type": "shell",
+ "command": "${workspaceFolder}/build-input/bazel-8.4.2-darwin-arm64",
+ "args": [
+ "build",
+ "Telegram/Telegram",
+ "--features=swift.use_global_module_cache",
+ "--verbose_failures",
+ "--remote_cache_async",
+ "--features=swift.skip_function_bodies_for_derived_files",
+ "--jobs=16",
+ "--define=buildNumber=10000",
+ "--define=telegramVersion=12.2.2",
+ "--disk_cache=${HOME}/telegram-bazel-cache",
+ "-c",
+ "dbg",
+ "--ios_multi_cpus=sim_arm64",
+ "--watchos_cpus=arm64_32",
+ "--features=swift.enable_batch_mode"
+ ],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ // Hidden never-ending task that handles the launch / debugging bits for Cmd+Shift+D.
+ // The problemMatcher field defines when the task is effectively ready to be debugged
+ // by the attach task in launch.json.
+ {
+ "label": "_launch_telegram",
+ "type": "shell",
+ "command": "./scripts/launch_and_debug.sh",
+ "presentation": {
+ "reveal": "always"
+ },
+ "hide": true,
+ "isBackground": true,
+ "problemMatcher": [
+ {
+ "pattern": [
+ {
+ "regexp": "\\b\\B",
+ "file": 1,
+ "location": 2,
+ "message": 3
+ }
+ ],
+ "background": {
+ "activeOnStart": true,
+ "beginsPattern": "^.*Building....*",
+ "endsPattern": "^.*Listening to port 6667 for a connection from .*"
+ }
+ }
+ ],
+ "runOptions": {
+ "instanceLimit": 1
+ }
+ },
+ {
+ "label": "Stream SourceKitBazelBSP Logs",
+ "type": "shell",
+ "command": "log stream --process sourcekit-bazel-bsp --debug",
+ "problemMatcher": [],
+ "isBackground": false,
+ "presentation": {
+ "reveal": "always",
+ "panel": "dedicated"
+ }
+ },
+ {
+ "label": "Stop Telegram Debug Session",
+ "type": "shell",
+ "command": "pkill -f 'launch_and_debug.sh' || true; lsof -ti:6667 | xargs kill -9 2>/dev/null || true",
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "silent",
+ "close": true
+ }
+ },
+ {
+ "label": "Restart Telegram Debug",
+ "dependsOrder": "sequence",
+ "dependsOn": [
+ "Stop Telegram Debug Session",
+ "_launch_telegram"
+ ],
+ "problemMatcher": [],
+ "presentation": {
+ "reveal": "always"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 0000000000..37688a7cca
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@sourcekit_bazel_bsp//rules:setup_sourcekit_bsp.bzl", "setup_sourcekit_bsp")
+
+setup_sourcekit_bsp(
+ name = "setup_sourcekit_bsp_telegram_project",
+ bazel_wrapper = "./build-input/bazel-8.4.2-darwin-arm64",
+ files_to_watch = [
+ "**/*.swift",
+ ],
+ index_flags = [
+ "config=index_build",
+ ],
+ targets = [
+ "//Telegram:Telegram",
+ ],
+)
diff --git a/MODULE.bazel b/MODULE.bazel
index a9aae97daf..f8ca9964e1 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -30,8 +30,8 @@ local_path_override(
http_file(
name = "cmake_tar_gz",
- urls = ["https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1-macos-universal.tar.gz"],
- sha256 = "f794ed92ccb4e9b6619a77328f313497d7decf8fb7e047ba35a348b838e0e1e2",
+ urls = ["https://github.com/Kitware/CMake/releases/download/v4.1.2/cmake-4.1.2-macos-universal.tar.gz"],
+ sha256 = "3be85f5b999e327b1ac7d804cbc9acd767059e9f603c42ec2765f6ab68fbd367",
)
http_file(
@@ -62,3 +62,9 @@ local_path_override(
module_name = "build_configuration",
path = "./build-input/configuration-repository",
)
+
+bazel_dep(name = "sourcekit_bazel_bsp", version = "0.3.0")
+local_path_override(
+ module_name = "sourcekit_bazel_bsp",
+ path = "build-system/bazel-rules/sourcekit-bazel-bsp",
+)
\ No newline at end of file
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 6e376e79fa..ff6647515f 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -98,8 +98,8 @@
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
"https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
- "https://bcr.bazel.build/modules/rules_java/8.12.0/MODULE.bazel": "8e6590b961f2defdfc2811c089c75716cb2f06c8a4edeb9a8d85eaa64ee2a761",
- "https://bcr.bazel.build/modules/rules_java/8.12.0/source.json": "cbd5d55d9d38d4008a7d00bee5b5a5a4b6031fcd4a56515c9accbcd42c7be2ba",
+ "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615",
+ "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
@@ -156,7 +156,7 @@
"moduleExtensions": {
"@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
"general": {
- "bzlTransitiveDigest": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=",
+ "bzlTransitiveDigest": "OlvsB0HsvxbR8ZN+J9Vf00X/+WVz/Y/5Xrq2LgcVfdo=",
"usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -256,7 +256,7 @@
},
"@@rules_xcodeproj+//xcodeproj:extensions.bzl%internal": {
"general": {
- "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=",
+ "bzlTransitiveDigest": "hsJI1bzu5CX3ZE+NX8jnNWEOh1pNDT650SbTSBnF5jA=",
"usagesDigest": "fvsnMonVwKDYnBfww4bXuYie3WU0d9VSqT2gePSdQco=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -278,7 +278,7 @@
},
"@@rules_xcodeproj+//xcodeproj:extensions.bzl%non_module_deps": {
"general": {
- "bzlTransitiveDigest": "/r7IWMTdpceHqqiSh7G9bQLHvIkfqE/cesswbkTSJZw=",
+ "bzlTransitiveDigest": "hsJI1bzu5CX3ZE+NX8jnNWEOh1pNDT650SbTSBnF5jA=",
"usagesDigest": "jzxYhnOC9BE0dJ0biFLfxWXi/+R19uAAZkJ0p9CY0JI=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
diff --git a/Telegram/BUILD b/Telegram/BUILD
index 7ef735898f..74b3ecae94 100644
--- a/Telegram/BUILD
+++ b/Telegram/BUILD
@@ -495,6 +495,8 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id
applinks:telegram.me
applinks:t.me
applinks:*.t.me
+ webcredentials:t.me
+ webcredentials:telegram.org
"""
diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings
index 44e63f3062..ab4058955f 100644
--- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings
+++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings
@@ -15432,3 +15432,194 @@ Error: %8$@";
"LiveStream.ErrorMaxAllowedEmoji.Text_1" = "You can send up to %d emoji.";
"LiveStream.ErrorMaxAllowedEmoji.Text_any" = "You can send up to %d emoji.";
+
+"Stars.Purchase.StarGiftOfferInfo" = "Buy Stars to make a gift buy offer.";
+
+"Notification.StarsGiftOffer.Offer" = "%1$@ offered you %2$@ for your gift %3$@";
+"Notification.StarsGiftOffer.Offer.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.Offer.Stars_any" = "%@ Stars";
+"Notification.StarsGiftOffer.OfferYou" = "You offered %1$@ %2$@ for their gift %3$@";
+"Notification.StarsGiftOffer.OfferYou.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.OfferYou.Stars_any" = "%@ Stars";
+
+"Notification.StarsGiftOffer.Accepted" = "%1$@ accepted your offer and sold you %2$@ for %3$@";
+"Notification.StarsGiftOffer.Accepted.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.Accepted.Stars_any" = "%@ Stars";
+"Notification.StarsGiftOffer.AcceptedYou" = "You sold %1$@ to %2$@ for %3$@";
+"Notification.StarsGiftOffer.AcceptedYou.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.AcceptedYou.Stars_any" = "%@ Stars";
+
+"Notification.StarsGiftOffer.Rejected" = "%1$@ rejected your offer for %2$@ – your %3$@ have been refunded";
+"Notification.StarsGiftOffer.Rejected.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.Rejected.Stars_any" = "%@ Stars";
+"Notification.StarsGiftOffer.RejectedYou" = "You rejected %1$@'s offer";
+"Notification.StarsGiftOffer.RejectedYou.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.RejectedYou.Stars_any" = "%@ Stars";
+
+"Notification.StarsGiftOffer.Expired" = "%1$@ didn't respond to your offer for %2$@ within %3$@ – your %4$@ have been refunded";
+"Notification.StarsGiftOffer.Expired.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.Expired.Stars_any" = "%@ Stars";
+"Notification.StarsGiftOffer.ExpiredYou" = "The offer from %1$@ to buy your %2$@ for %3$@ has expired";
+"Notification.StarsGiftOffer.ExpiredYou.Stars_1" = "%@ Star";
+"Notification.StarsGiftOffer.ExpiredYou.Stars_any" = "%@ Stars";
+
+"CreateExternalStream.RevokeStreamKey" = "Revoke Stream Key";
+"CreateExternalStream.Revoke.Text" = "Do you want to revoke the stream key?";
+"CreateExternalStream.Revoke.Revoke" = "Revoke";
+
+"Gift.AuctionBid.UntilEnd" = "until the end";
+"Gift.AuctionBid.BeforeStart" = "before start";
+"Gift.AuctionBid.RoundSubtitle" = "Round %@ of %@";
+"Gift.AuctionBid.TopWinnersTotal" = "Top 3 of %@ Winners";
+
+"Gift.Acquired.GiftRound" = "%1$@ in round %2$@";
+
+"Gift.Options.Gift.Soon" = "soon";
+
+"Chat.Auction.StartsIn" = "starts in ";
+"Chat.Auction.View" = "VIEW";
+"Chat.Auction.Upcoming" = "Upcoming Auction";
+
+"Gift.Auction.Start" = "Start";
+"Gift.Auction.End" = "End";
+"Gift.Auction.Quantity" = "Quantity";
+"Gift.Auction.TotalRounds" = "Rounds";
+"Gift.Auction.TimeRound" = "Round %@";
+"Gift.Auction.TimeRounds" = "Rounds %@";
+"Gift.Auction.StartsInHours" = "auction starts in {h}:{m}:{s}";
+"Gift.Auction.StartsInMinutes" = "auction starts in {m}:{s}";
+"Gift.Auction.EarlyBid" = "Place an Early Bid";
+"Gift.Auction.ViewVariants" = "View";
+"Gift.Auction.Variants_1" = "%@ Variant";
+"Gift.Auction.Variants_any" = "%@ Variants";
+
+"Gift.Auction.Hours_1" = "%@ hour";
+"Gift.Auction.Hours_any" = "%@ hours";
+"Gift.Auction.Minutes_1" = "%@ minute";
+"Gift.Auction.Minutes_any" = "%@ minutes";
+"Gift.Auction.Seconds_1" = "%@ second";
+"Gift.Auction.Seconds_any" = "%@ seconds";
+"Gift.Auction.HoursEach_1" = "%@ hour each";
+"Gift.Auction.HoursEach_any" = "%@ hours each";
+"Gift.Auction.MinutesEach_1" = "%@ minute each";
+"Gift.Auction.MinutesEach_any" = "%@ minutes each";
+"Gift.Auction.Extension" = "+ %1$@ for late bids in top %2$@";
+
+"ChatList.Auctions.UpcomingAuction" = "Upcoming Auction";
+
+"Gift.AuctionBid.UpcomingBid" = "You've placed an early bid";
+"ChatList.Auctions.Status.UpcomingBid" = "You've placed an early bid.";
+"Gift.ActiveAuctions.UpcomingBid" = "You've placed an early bid";
+
+"Login.PhoneWithPasskeySubtitle" = "Enter your phone number\nor [log in using Passkey >](passkey)";
+"PrivacySettings.Passkey" = "Passkey";
+"Attachment.SharedAudio" = "SHARED AUDIO";
+"Attachment.FilesSearchPlaceholder" = "Search shared audio";
+"Chat.GiftPurchaseOffer.Reject" = "Reject";
+"Chat.GiftPurchaseOffer.Accept" = "Accept";
+"Chat.GiftPurchaseOffer.RejectConfirmation.Title" = "Reject Offer";
+"Chat.GiftPurchaseOffer.RejectConfirmation.Text" = "Are you sure you want to reject the offer from **%@**?";
+"Chat.GiftPurchaseOffer.RejectConfirmation.Reject" = "Reject";
+
+"Passkeys.DeleteAlert.Title" = "Delete Passkey?";
+"Passkeys.DeleteAlert.Text" = "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.";
+"Passkeys.DeleteAlert.Action" = "Delete";
+"Passkeys.Into.Title" = "Protect your account";
+"Passkeys.Subtitle" = "Log in safely and keep your account secure.";
+"Passkeys.Into.Title0" = "Create a Passkey";
+"Passkeys.Into.Text0" = "Make a passkey to sign in easily and safely.";
+"Passkeys.Into.Title1" = "Log in with Face ID";
+"Passkeys.Into.Text1" = "Use Face ID, Touch ID, or your passcode to sign in.";
+"Passkeys.Into.Title2" = "Store Passkey Securely";
+"Passkeys.Into.Text2" = "Your passkey is safely kept in your iCloud Keychain.";
+"Passkeys.ButtonCreate" = "Create Passkey";
+"Passkeys.ButtonSkip" = "Skip";
+"Passkeys.Title" = "Passkeys";
+"Passkeys.Subtitle" = "Log in safely and keep your account secure.";
+"Passkeys.EmptyName" = "Passkey";
+"Passkeys.PasskeyCreatedPattern" = "created %@";
+"Passkeys.PasskeyCreatedAndUsedPattern" = "created %1$@ • used %2$@";
+"Passkeys.AddPasskey" = "Create Passkey";
+"Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager.";
+
+"Gift.Demo.Title" = "Unique Collectibles";
+"Gift.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile.";
+"Gift.Demo.Unique.Title" = "Unique";
+"Gift.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol.";
+"Gift.Demo.Tradable.Title" = "Tradable";
+"Gift.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces.";
+"Gift.Demo.Wearable.Title" = "Wearable";
+"Gift.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses.";
+"Gift.Demo.Understood" = "Understood";
+
+"Gift.Variants.RandomTraits" = "Random Traits";
+"Gift.Variants.SelectedTraits" = "Selected Traits";
+"Gift.Variants.Randomize" = "Randomize Traits";
+"Gift.Variants.Model" = "model";
+"Gift.Variants.Backdrop" = "backdrop";
+"Gift.Variants.Symbol" = "symbol";
+"Gift.Variants.Models" = "Models";
+"Gift.Variants.Backdrops" = "Backdrops";
+"Gift.Variants.Symbols" = "Symbols";
+
+"Gift.Variants.CollectionInfo" = "This collection features %@";
+"Gift.Variants.CollectionInfo.Model_1" = "**%@** unique model";
+"Gift.Variants.CollectionInfo.Model_any" = "**%@** unique models";
+"Gift.Variants.CollectionInfo.Backdrop_1" = "**%@** unique backdrop";
+"Gift.Variants.CollectionInfo.Backdrop_any" = "**%@** unique backdrops";
+"Gift.Variants.CollectionInfo.Symbol_1" = "**%@** unique symbol";
+"Gift.Variants.CollectionInfo.Symbol_any" = "**%@** unique symbols";
+
+"Gift.Auction.GiftAuction" = "Upcoming Auction";
+"Gift.Auction.UpcomingAuction" = "Upcoming Auction";
+"Gift.Auction.LearnMore" = "Learn more about Telegram Gifts >";
+
+"Gift.AuctionBid.UpcomingTitle" = "Place an Early Bid";
+
+"Gift.View.Context.BuyOffer" = "Offer to Buy";
+
+"Gift.Offer.Title" = "Offer to Buy";
+"Gift.Offer.OfferStars" = "Offer Stars";
+"Gift.Offer.OfferTon" = "Offer TON";
+"Gift.Offer.PriceSectionStars" = "STARS TO OFFER";
+"Gift.Offer.PriceSectionTon" = "TON TO OFFER";
+"Gift.Offer.PricePlaceholder" = "Price";
+"Gift.Offer.PriceDescriptionStars" = "Enter the number of Stars you'd like to offer for **%@**.";
+"Gift.Offer.PriceDescriptionTon" = "Enter the number of TON you'd like to offer for **%@**.";
+"Gift.Offer.Duration" = "Offer Duration";
+"Gift.Offer.DurationDescriptionStars" = "Choose how long **%@** can accept your offer. When the time expires, your Stars will be automatically refunded.";
+"Gift.Offer.DurationDescriptionTon" = "Choose how long **%@** can accept your offer. When the time expires, your TON will be automatically refunded.";
+"Gift.Offer.Duration.Hours_1" = "%@ Hour";
+"Gift.Offer.Duration.Hours_any" = "%@ Hours";
+"Gift.Offer.Offer" = "Offer";
+"Gift.Offer.GiftMinAmountToast.Text" = "You cannot offer less than %@ for this gift";
+
+"Gift.WearPreview.Limited" = "limited";
+"Gift.WearPreview.Upgraded" = "upgraded";
+"Gift.WearPreview.FreeUpgrade" = "Free\nUpgrade";
+"Gift.WearPreview.LearnMore" = "Learn more about wearing Telegram Gifts >";
+
+"Notification.StarGift.Sold" = "sold";
+"Notification.StarGiftOffer.Offer" = "**%1$@** offered you **%2$@** for your gift **%3$@**.";
+"Notification.StarGiftOffer.OfferYou" = "You offered **%1$@** **%2$@** for their gift **%3$@**.";
+"Notification.StarGiftOffer.Offer.Stars_1" = "%@ Star";
+"Notification.StarGiftOffer.Offer.Stars_any" = "%@ Stars";
+
+"Notification.StarGiftOffer.Status.Accepted" = "This offer was accepted.";
+"Notification.StarGiftOffer.Status.Expired" = "This offer has expired.";
+"Notification.StarGiftOffer.Status.Expires" = "This offer expires in %@";
+"Notification.StarGiftOffer.Status.Rejected" = "This offer was rejected.";
+
+"Notification.StarGiftOffer.Expiration.Hours_1" = "%@ h";
+"Notification.StarGiftOffer.Expiration.Hours_any" = "%@ h";
+"Notification.StarGiftOffer.Expiration.Minutes_1" = "%@ m";
+"Notification.StarGiftOffer.Expiration.Minutes_any" = "%@ m";
+"Notification.StarGiftOffer.Expiration.Delimiter" = "";
+
+"Chat.GiftPurchaseOffer.AcceptConfirmation.Title" = "Confirm Sale";
+"Chat.GiftPurchaseOffer.AcceptConfirmation.Text" = "Do you want to sell **%1$@** to %2$@ for **%3$@**? You'll receive **%4$@** after fees.";
+"Chat.GiftPurchaseOffer.AcceptConfirmation.Text.Stars_1" = "%@ Star";
+"Chat.GiftPurchaseOffer.AcceptConfirmation.Text.Stars_any" = "%@ Stars";
+"Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer.";
+"Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale";
+
diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py
index 1bdd0f7412..1898ad5cb4 100644
--- a/build-system/Make/Make.py
+++ b/build-system/Make/Make.py
@@ -40,7 +40,6 @@ class BazelCommandLine:
self.additional_args = None
self.build_number = None
self.configuration_args = None
- self.configuration_path = None
self.split_submodules = False
self.custom_target = None
self.continue_on_error = False
@@ -68,7 +67,7 @@ class BazelCommandLine:
'--verbose_failures',
# Asynchronously upload cache artifacts
- '--experimental_remote_cache_async',
+ '--remote_cache_async',
]
self.common_build_args = [
@@ -89,12 +88,6 @@ class BazelCommandLine:
# invoking a smaller number of frontend processes and passing them batches of
# source files.
'--features=swift.enable_batch_mode',
-
- # https://docs.bazel.build/versions/master/command-line-reference.html
- # Set the number of parallel jobs per module to saturate the available CPU resources.
- #'--swiftcopt=-j{}'.format(os.cpu_count() - 1),
- '--@build_bazel_rules_swift//swift:copt="-j{}"'.format(os.cpu_count() - 1),
- '--@build_bazel_rules_swift//swift:copt="-whole-module-optimization"',
]
self.common_release_args = [
@@ -147,9 +140,6 @@ class BazelCommandLine:
def set_split_swiftmodules(self, value):
self.split_submodules = value
- def set_configuration_path(self, path):
- self.configuration_path = path
-
def set_disable_provisioning_profiles(self):
self.disable_provisioning_profiles = True
@@ -203,8 +193,6 @@ class BazelCommandLine:
# Require DSYM files as build output.
'--output_groups=+dsyms',
-
- #'--@build_bazel_rules_swift//swift:copt="-num-threads 0"',
] + self.common_release_args
else:
raise Exception('Unknown configuration {}'.format(configuration))
@@ -296,13 +284,6 @@ class BazelCommandLine:
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
- if self.configuration_path is None:
- raise Exception('configuration_path is not defined')
-
- combined_arguments += [
- '--override_repository=build_configuration={}'.format(self.configuration_path)
- ]
-
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
@@ -336,13 +317,6 @@ class BazelCommandLine:
combined_arguments += ['Tests/AllTests']
- if self.configuration_path is None:
- raise Exception('configuration_path is not defined')
-
- combined_arguments += [
- '--override_repository=build_configuration={}'.format(self.configuration_path)
- ]
-
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
@@ -371,13 +345,6 @@ class BazelCommandLine:
combined_arguments += self.get_startup_bazel_arguments()
combined_arguments += ['aquery']
- if self.configuration_path is None:
- raise Exception('configuration_path is not defined')
-
- combined_arguments += [
- '--override_repository=build_configuration={}'.format(self.configuration_path)
- ]
-
combined_arguments += [
'-c', 'dbg',
'--ios_multi_cpus=sim_arm64',
@@ -425,13 +392,6 @@ class BazelCommandLine:
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
- if self.configuration_path is None:
- raise Exception('configuration_path is not defined')
-
- combined_arguments += [
- '--override_repository=build_configuration={}'.format(self.configuration_path)
- ]
-
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
@@ -562,9 +522,6 @@ def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, argum
file.write(' "{}",\n'.format(file_name))
file.write('])\n')
- if bazel_command_line is not None:
- bazel_command_line.set_configuration_path(configuration_repository_path)
-
def generate_project(bazel, arguments):
bazel_command_line = BazelCommandLine(
@@ -617,7 +574,6 @@ def generate_project(bazel, arguments):
disable_provisioning_profiles=disable_provisioning_profiles,
include_release=project_include_release,
generate_dsym=generate_dsym,
- configuration_path=bazel_command_line.configuration_path,
bazel_app_arguments=bazel_command_line.get_project_generation_arguments(),
target_name=target_name
)
@@ -1006,14 +962,10 @@ if __name__ == '__main__':
buildParser.add_argument(
'--configuration',
choices=[
- 'debug_universal',
'debug_arm64',
- 'debug_armv7',
'debug_sim_arm64',
'release_sim_arm64',
'release_arm64',
- 'release_armv7',
- 'release_universal'
],
required=True,
help='Build configuration'
@@ -1254,14 +1206,10 @@ if __name__ == '__main__':
spm_parser.add_argument(
'--configuration',
choices=[
- 'debug_universal',
'debug_arm64',
- 'debug_armv7',
'debug_sim_arm64',
'release_sim_arm64',
'release_arm64',
- 'release_armv7',
- 'release_universal'
],
required=True,
help='Build configuration'
diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py
index 17d436a33d..8de469312c 100644
--- a/build-system/Make/ProjectGeneration.py
+++ b/build-system/Make/ProjectGeneration.py
@@ -9,7 +9,7 @@ def remove_directory(path):
if os.path.isdir(path):
shutil.rmtree(path)
-def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name):
+def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, bazel_app_arguments, target_name):
if '/' in target_name:
app_target_spec = target_name.split('/')[0] + '/' + target_name.split('/')[1] + ':' + target_name.split('/')[1]
app_target = target_name
@@ -22,7 +22,6 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
bazel_generate_arguments = [build_environment.bazel_path]
bazel_generate_arguments += ['run', '//{}_xcodeproj'.format(app_target_spec)]
- bazel_generate_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
if target_name == 'Telegram':
if disable_extensions:
@@ -32,7 +31,6 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
project_bazel_arguments = []
for argument in bazel_app_arguments:
project_bazel_arguments.append(argument)
- project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
if target_name == 'Telegram':
if disable_extensions:
@@ -54,5 +52,5 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
return xcodeproj_path
-def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name) -> str:
- return generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name)
+def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, bazel_app_arguments, target_name) -> str:
+ return generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, bazel_app_arguments, target_name)
diff --git a/build-system/bazel-rules/sourcekit-bazel-bsp b/build-system/bazel-rules/sourcekit-bazel-bsp
new file mode 160000
index 0000000000..fb37d850e8
--- /dev/null
+++ b/build-system/bazel-rules/sourcekit-bazel-bsp
@@ -0,0 +1 @@
+Subproject commit fb37d850e8715df81761feeea25f2158514fa2be
diff --git a/build-system/decrypt.rb b/build-system/decrypt.rb
index 5f89736a25..1183ebf40d 100644
--- a/build-system/decrypt.rb
+++ b/build-system/decrypt.rb
@@ -52,9 +52,9 @@ class EncryptionV2
iv = keyIv[32..43]
auth_data = keyIv[44..-1]
- puts "key: #{key.inspect}"
- puts "iv: #{iv.inspect}"
- puts "auth_data: #{auth_data.inspect}"
+ #puts "key: #{key.inspect}"
+ #puts "iv: #{iv.inspect}"
+ #puts "auth_data: #{auth_data.inspect}"
cipher.key = key
cipher.iv = iv
diff --git a/scripts/Telegram b/scripts/Telegram
new file mode 100755
index 0000000000..efd83a0c5d
--- /dev/null
+++ b/scripts/Telegram
@@ -0,0 +1,755 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Invoked by `bazel run` to launch *_application targets in the simulator."""
+
+# This script works in one of two modes.
+#
+# If either --ios_simulator_version or --ios_simulator_device were not
+# passed to bazel:
+#
+# 1. Discovers a simulator compatible with the minimum_os of the
+# *_application target, preferring already-booted simulators
+# if possible
+# 2. Boots the simulator if needed
+# 3. Installs and launches the application
+# 4. Displays the application's output on the console
+#
+# This mode does not kill running simulators or shutdown or delete the simulator
+# after it completes.
+#
+# If --ios_simulator_version and --ios_simulator_device were both passed
+# to bazel:
+#
+# 1. Creates a new temporary simulator by running "simctl create ..."
+# 2. Boots the new temporary simulator
+# 3. Installs and launches the application
+# 4. Displays the application's output on the console
+# 5. When done, shuts down and deletes the newly-created simulator
+#
+# All environment variables with names starting with "IOS_" are passed to the
+# application, after stripping the prefix "IOS_".
+
+import collections.abc
+import contextlib
+import json
+import logging
+import os
+import os.path
+import pathlib
+import platform
+import plistlib
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+from typing import Dict, Optional
+import zipfile
+
+
+# Custom type for methods yielding an Apple simulator UDID.
+AppleSimulatorUDID = collections.abc.Generator[str, None, None]
+
+
+logging.basicConfig(
+ format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ level=logging.INFO,
+)
+logger = logging.getLogger(__name__)
+
+if platform.system() != "Darwin":
+ raise Exception(
+ "Cannot run Apple platform application targets on a non-mac machine."
+ )
+
+
+class DeviceType(collections.abc.Mapping):
+ """Wraps the `devicetype` dictionary from `simctl list -j`.
+
+ Provides an ordering so iPhones > iPads. In addition, maintains the
+ original order from `simctl list` as `simctl_list_index` to ensure
+ newer device types are sorted after older device types.
+ """
+
+ def __init__(self, device_type, simctl_list_index):
+ self.device_type = device_type
+ self.simctl_list_index = simctl_list_index
+
+ def __getitem__(self, name):
+ return self.device_type[name]
+
+ def __iter__(self):
+ return iter(self.device_type)
+
+ def __len__(self):
+ return len(self.device_type)
+
+ def __repr__(self):
+ return self["name"] + " (" + self["identifier"] + ")"
+
+ def __lt__(self, other):
+ # Order iPhones ahead of (later in the list than) iPads.
+ if self.is_ipad() and other.is_iphone():
+ return True
+ elif self.is_iphone() and other.is_ipad():
+ return False
+ # Order device types from the same product family in the same order
+ # as `simctl list`.
+ return self.simctl_list_index < other.simctl_list_index
+
+ def supports_platform_type(self, platform_type: str) -> bool:
+ """Returns boolean to indicate if device supports given Apple platform type."""
+ if platform_type == "ios":
+ return self.is_iphone() or self.is_ipad()
+ elif platform_type == "tvos":
+ return self.is_apple_tv()
+ elif platform_type == "watchos":
+ return self.is_apple_watch()
+ elif platform_type == "visionos":
+ return self.is_apple_vision()
+ else:
+ raise ValueError(
+ f"Apple platform type not supported for simulator: {platform_type}."
+ )
+
+ def is_apple_tv(self) -> bool:
+ return self.has_product_family_or_identifier("Apple TV")
+
+ def is_apple_watch(self) -> bool:
+ return self.has_product_family_or_identifier("Apple Watch")
+
+ def is_apple_vision(self) -> bool:
+ return self.has_product_family_or_identifier("Apple Vision")
+
+ def is_iphone(self) -> bool:
+ return self.has_product_family_or_identifier("iPhone")
+
+ def is_ipad(self) -> bool:
+ return self.has_product_family_or_identifier("iPad")
+
+ def has_product_family_or_identifier(self, device_type: str) -> bool:
+ product_family = self.get("productFamily")
+ if product_family:
+ return product_family == device_type
+ # Some older simulators are missing `productFamily`. Try to guess from the
+ # identifier.
+ return device_type in self["identifier"]
+
+
+class Device(collections.abc.Mapping):
+ """Wraps the `device` dictionary from `simctl list -j`.
+
+ Provides an ordering so booted devices > shutdown devices, delegating
+ to `DeviceType` order when both devices have the same state.
+ """
+
+ def __init__(self, device, device_type):
+ self.device = device
+ self.device_type = device_type
+
+ def is_shutdown(self):
+ return self["state"] == "Shutdown"
+
+ def is_booted(self):
+ return self["state"] == "Booted"
+
+ def __getitem__(self, name):
+ return self.device[name]
+
+ def __iter__(self):
+ return iter(self.device)
+
+ def __len__(self):
+ return len(self.device)
+
+ def __repr__(self):
+ return self["name"] + "(" + self["udid"] + ")"
+
+ def __lt__(self, other):
+ if self.is_shutdown() and other.is_booted():
+ return True
+ elif self.is_booted() and other.is_shutdown():
+ return False
+ else:
+ return self.device_type < other.device_type
+
+
+def minimum_os_to_simctl_runtime_version(minimum_os: str) -> int:
+ """Converts a minimum OS string to a simctl RuntimeVersion integer.
+
+ Args:
+ minimum_os: A string in the form '12.2' or '13.2.3'.
+
+ Returns:
+ An integer in the form 0xAABBCC, where AA is the major version, BB is
+ the minor version, and CC is the micro version.
+ """
+ # Pad the minimum OS version to major.minor.micro.
+ minimum_os_components = (minimum_os.split(".") + ["0"] * 3)[:3]
+ result = 0
+ for component in minimum_os_components:
+ result = (result << 8) | int(component)
+ return result
+
+
+def discover_best_compatible_simulator(
+ *,
+ platform_type: str,
+ simctl_path: str,
+ minimum_os: str,
+ sim_device: str,
+ sim_os_version: str,
+) -> (Optional[DeviceType], Optional[Device]):
+ """Discovers the best compatible simulator device type and device.
+
+ Args:
+ platform_type: The Apple platform type for the given *_application() target.
+ simctl_path: The path to the `simctl` binary.
+ minimum_os: The minimum OS version required by the *_application() target.
+ sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
+ sim_os_version: Optional version of the Apple platform runtime (e.g.
+ "13.2").
+
+ Returns:
+ A tuple (device_type, device) containing the DeviceType and Device
+ of the best compatible simulator (might be None if no match was found).
+
+ Raises:
+ subprocess.SubprocessError: if `simctl list` fails or times out.
+ """
+ # The `simctl list` CLI provides only very basic case-insensitive description
+ # matching search term functionality.
+ #
+ # This code needs to enforce a numeric floor on `minimum_os`, so it directly
+ # parses the JSON output by `simctl list` instead of repeatedly invoking
+ # `simctl list` with search terms.
+ cmd = [simctl_path, "list", "-j"]
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE) as process:
+ simctl_data = json.load(process.stdout)
+ if process.wait() != os.EX_OK:
+ raise subprocess.CalledProcessError(process.returncode, cmd)
+ compatible_device_types = []
+ minimum_runtime_version = minimum_os_to_simctl_runtime_version(minimum_os)
+ # Prepare the device name for case-insensitive matching.
+ sim_device = sim_device and sim_device.casefold()
+ # `simctl list` orders device types from oldest to newest. Remember
+ # the index of each device type to preserve that ordering when
+ # sorting device types.
+ for simctl_list_index, device_type in enumerate(simctl_data["devicetypes"]):
+ device_type = DeviceType(device_type, simctl_list_index)
+ if not device_type.supports_platform_type(platform_type):
+ continue
+ # Some older simulators are missing `maxRuntimeVersion`. Assume those
+ # simulators support all OSes (even though it's not true).
+ max_runtime_version = device_type.get("maxRuntimeVersion")
+ if max_runtime_version and max_runtime_version < minimum_runtime_version:
+ continue
+ if sim_device and device_type["name"].casefold() != sim_device:
+ continue
+ compatible_device_types.append(device_type)
+ compatible_device_types.sort()
+ # logger.info(
+ # "Found %d potentialcompatible device types.", len(compatible_device_types)
+ # )
+ compatible_runtime_identifiers = set()
+ for runtime in simctl_data["runtimes"]:
+ if not runtime["isAvailable"]:
+ continue
+ if sim_os_version and runtime["version"] != sim_os_version:
+ continue
+ compatible_runtime_identifiers.add(runtime["identifier"])
+ compatible_devices = []
+ for runtime_identifier, devices in simctl_data["devices"].items():
+ if runtime_identifier not in compatible_runtime_identifiers:
+ continue
+ for device in devices:
+ if not device["isAvailable"]:
+ continue
+ compatible_device = None
+ for device_type in compatible_device_types:
+ if device["deviceTypeIdentifier"] == device_type["identifier"]:
+ compatible_device = Device(device, device_type)
+ break
+ if not compatible_device:
+ continue
+ compatible_devices.append(compatible_device)
+ compatible_devices.sort()
+ logger.debug("Found %d compatible devices.", len(compatible_devices))
+ if compatible_device_types:
+ best_compatible_device_type = compatible_device_types[-1]
+ else:
+ best_compatible_device_type = None
+ if compatible_devices:
+ best_compatible_device = compatible_devices[-1]
+ else:
+ best_compatible_device = None
+ return (best_compatible_device_type, best_compatible_device)
+
+
+def persistent_simulator(
+ *,
+ platform_type: str,
+ simctl_path: str,
+ minimum_os: str,
+ sim_device: str,
+ sim_os_version: str,
+) -> str:
+ """Finds or creates a persistent compatible Apple simulator.
+
+ Boots the simulator if needed. Does not shut down or delete the simulator when
+ done.
+
+ Args:
+ platform_type: The Apple platform type for the given *_application() target.
+ simctl_path: The path to the `simctl` binary.
+ minimum_os: The minimum OS version required by the *_application() target.
+ sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
+ sim_os_version: Optional version of the Apple platform runtime (e.g.
+ "13.2").
+
+ Returns:
+ The UDID of the compatible Apple simulator.
+
+ Raises:
+ Exception: if a compatible simulator was not found.
+ """
+ (best_compatible_device_type, best_compatible_device) = (
+ discover_best_compatible_simulator(
+ platform_type=platform_type,
+ simctl_path=simctl_path,
+ minimum_os=minimum_os,
+ sim_device=sim_device,
+ sim_os_version=sim_os_version,
+ )
+ )
+ if best_compatible_device:
+ udid = best_compatible_device["udid"]
+ if best_compatible_device.is_shutdown():
+ logger.debug("Booting compatible device: %s", best_compatible_device)
+ subprocess.run([simctl_path, "boot", udid], check=True)
+ else:
+ logger.debug("Using compatible device: %s", best_compatible_device)
+ return udid
+ if best_compatible_device_type:
+ device_name = best_compatible_device_type["name"]
+ device_id = best_compatible_device_type["identifier"]
+ # logger.info("Creating new %s simulator", device_name)
+ create_result = subprocess.run(
+ [simctl_path, "create", device_name, device_id],
+ encoding="utf-8",
+ stdout=subprocess.PIPE,
+ check=True,
+ )
+ udid = create_result.stdout.rstrip()
+ logger.debug("Created new simulator: %s", udid)
+ return udid
+ raise Exception(
+ f"Could not find or create a simulator for the {platform_type} platform"
+ f"compatible with minimum OS version {minimum_os} (device name "
+ f"{sim_device}, OS version {sim_os_version})"
+ )
+
+
+def wait_for_sim_to_boot(simctl_path: str, udid: str) -> bool:
+ """Blocks until the given simulator is booted.
+
+ Args:
+ simctl_path: The path to the `simctl` binary.
+ udid: The identifier of the simulator to wait for.
+
+ Returns:
+ True if the simulator boots within 60 seconds, False otherwise.
+ """
+ logger.info("Waiting for simulator to boot...")
+ for _ in range(0, 60):
+ # The expected output of "simctl list" is like:
+ # -- iOS 8.4 --
+ # iPhone 5s (E946FA1C-26AB-465C-A7AC-24750D520BEA) (Shutdown)
+ # TestDevice (8491C4BC-B18E-4E2D-934A-54FA76365E48) (Booted)
+ # So if there's any booted simulator, $booted_device will not be empty.
+ #logger.info("will list devices with udid: %s", udid)
+ simctl_list_result = subprocess.run(
+ [simctl_path, "list", "devices"],
+ encoding="utf-8",
+ check=True,
+ stdout=subprocess.PIPE,
+ )
+ #logger.info(simctl_list_result.stdout)
+ for line in simctl_list_result.stdout.split("\n"):
+ if line.find(udid) != -1 and line.find("Booted") != -1:
+ logger.debug("Simulator is booted.")
+ # Simulator is booted.
+ return True
+ logger.debug("Simulator not booted, still waiting...")
+ time.sleep(1)
+ return False
+
+
+def boot_simulator(*, developer_path: str, simctl_path: str, udid: str) -> None:
+ """Launches the Apple simulator for the given identifier.
+
+ Ensures the Simulator process is in the foreground.
+
+ Args:
+ developer_path: The path to /Applications/Xcode.app/Contents/Developer.
+ simctl_path: The path to the `simctl` binary.
+ udid: The identifier of the simulator to wait for.
+
+ Raises:
+ Exception: if the simulator did not launch within 60 seconds.
+ """
+ logger.info("Launching simulator with udid: %s", udid)
+ # Using subprocess.Popen() to launch Simulator.app and then
+ # `osascript -e "tell application \"Simulator\" to activate" is racy
+ # and can fail with:
+ #
+ # Simulator got an error: Connection is invalid. (-609)
+ #
+ # This is likely because the newly-spawned Simulator.app process
+ # hasn't had time to connect to the Apple Events system which
+ # `osascript` relies on.
+ simulator_path = os.path.join(developer_path, "Applications/Simulator.app")
+ subprocess.run(
+ ["open", "-a", simulator_path, "--args", "-CurrentDeviceUDID", udid],
+ check=True,
+ )
+ logger.debug("Simulator launched.")
+ if not wait_for_sim_to_boot(simctl_path, udid):
+ raise Exception("Failed to launch simulator with UDID: " + udid)
+
+
+@contextlib.contextmanager
+def temporary_simulator(
+ *, platform_type: str, simctl_path: str, device: str, version: str
+) -> AppleSimulatorUDID:
+ """Creates a temporary Apple simulator, cleaned up automatically upon close.
+
+ Args:
+ platform_type: The Apple platform type for the given *_application() target.
+ simctl_path: The path to the `simctl` binary.
+ device: The name of the device (e.g. "iPhone 8 Plus").
+ version: The version of the Apple platform runtime (e.g. "13.2").
+
+ Yields:
+ The UDID of the newly-created Apple simulator.
+ """
+ runtime_version_name = version.replace(".", "-")
+ # capitalizes 'os' from Apple platform type string (e.g. watchos -> watchOS)
+ runtime_platform = platform_type[0:-2].lower() + platform_type[-2:].upper()
+ # logger.info("Creating simulator, device=%s, version=%s", device, version)
+ simctl_create_result = subprocess.run(
+ [
+ simctl_path,
+ "create",
+ "TestDevice",
+ device,
+ "{prefix}.{runtime_platform}-{runtime_version_name}".format(
+ prefix="com.apple.CoreSimulator.SimRuntime",
+ runtime_platform=runtime_platform,
+ runtime_version_name=runtime_version_name,
+ ),
+ ],
+ encoding="utf-8",
+ check=True,
+ stdout=subprocess.PIPE,
+ )
+ udid = simctl_create_result.stdout.rstrip()
+ try:
+ logger.info("Killing all running simulators...")
+ subprocess.run(
+ ["pkill", "Simulator"], stderr=subprocess.DEVNULL, check=False
+ )
+ yield udid
+ finally:
+ logger.info("Shutting down simulator with udid: %s", udid)
+ subprocess.run(
+ [simctl_path, "shutdown", udid], stderr=subprocess.DEVNULL, check=False
+ )
+ logger.info("Deleting simulator with udid: %s", udid)
+ subprocess.run([simctl_path, "delete", udid], check=True)
+
+
+def register_dsyms(dsyms_dir: str):
+ """Adds all dSYMs in `dsyms_dir` to the symbolscache.
+
+ Args:
+ dsyms_dir: Path to directory potentially containing dSYMs
+ """
+ symbolscache_command = [
+ "/usr/bin/symbolscache",
+ "delete",
+ "--tag",
+ "Bazel",
+ "compact",
+ "add",
+ "--tag",
+ "Bazel",
+ ] + [
+ a
+ for a in pathlib.Path(dsyms_dir).glob(
+ "**/*.dSYM/Contents/Resources/DWARF/*"
+ )
+ ]
+ logger.debug("Running command: %s", symbolscache_command)
+ result = subprocess.run(
+ symbolscache_command,
+ capture_output=True,
+ check=True,
+ encoding="utf-8",
+ text=True,
+ )
+ logger.debug("symbolscache output: %s", result.stdout)
+
+
+@contextlib.contextmanager
+def extracted_app(
+ application_output_path: str, app_name: str
+) -> AppleSimulatorUDID:
+ """Extracts Foo.app from *_application() output and makes it writable.
+
+ Args:
+ application_output_path: Path to the output of an `*_application()`. If the
+ path is a directory, copies it to a temporary directory and makes the
+ contents writable, as `simctl install` fails to install an `.app` that is
+ read-only. If the path is an .ipa archive, unzips it to a temporary
+ directory.
+ app_name: The name of the application (e.g. "Foo" for "Foo.app").
+
+ Yields:
+ Path to Foo.app in temporary directory (re-used if already present).
+ """
+ if os.path.isdir(application_output_path):
+ # Re-use the same path for each run and rsync to it (reducing
+ # copies). Ensure the result is writable, or `simctl install` will
+ # fail with `Unhandled error domain NSPOSIXErrorDomain, code 13`.
+ dst_dir = os.path.join(tempfile.gettempdir(), "bazel_temp_" + app_name)
+ os.makedirs(dst_dir, exist_ok=True)
+
+ # NOTE: use `which` to find the path to `rsync`.
+ # In macOS 15.4, the system `rsync` is using `openrsync` which contains some permission issues.
+ # This allows users to workaround the issue by overriding the system `rsync` with a working version.
+ # Remove this once we no longer support macOS versions with broken `rsync`.
+ rsync_path = shutil.which("rsync")
+
+ rsync_command = [
+ rsync_path,
+ "--archive",
+ "--delete",
+ "--checksum",
+ "--chmod=u+w",
+ "--verbose",
+ # The output path might itself be a symlink; resolve to the
+ # real path so rsync doesn't just copy the symlink.
+ os.path.realpath(application_output_path),
+ dst_dir,
+ ]
+ logger.debug(
+ "Found app directory: %s, running command: %s",
+ application_output_path,
+ rsync_command,
+ )
+ result = subprocess.run(
+ rsync_command,
+ capture_output=True,
+ check=True,
+ encoding="utf-8",
+ text=True,
+ )
+ logger.debug("rsync output: %s", result.stdout)
+ yield os.path.join(dst_dir, app_name + ".app")
+ else:
+ # Create a new temporary directory for each run, deleting it
+ # afterwards (there's no efficient way to "sync" an unzip, so this
+ # can't re-use the output directory).
+ with tempfile.TemporaryDirectory(prefix="bazel_temp") as temp_dir:
+ logger.debug(
+ "Unzipping IPA from %s to %s", application_output_path, temp_dir
+ )
+ with zipfile.ZipFile(application_output_path) as ipa_zipfile:
+ ipa_zipfile.extractall(temp_dir)
+ yield os.path.join(temp_dir, "Payload", app_name + ".app")
+
+
+def bundle_id(bundle_path: str) -> str:
+ """Returns the bundle ID given a bundle directory path."""
+ info_plist_path = os.path.join(bundle_path, "Info.plist")
+ with open(info_plist_path, mode="rb") as plist_file:
+ plist = plistlib.load(plist_file)
+ return plist["CFBundleIdentifier"]
+
+
+def simctl_launch_environ() -> Dict[str, str]:
+ """Calculates an environment dictionary for running `simctl launch`."""
+ # Pass environment variables prefixed with "IOS_" to the simulator, replace
+ # the prefix with "SIMCTL_CHILD_". bazel adds "IOS_" to the env vars which
+ # will be passed to the app as prefix to differentiate from other env vars. We
+ # replace the prefix "IOS_" with "SIMCTL_CHILD_" here, because "simctl" only
+ # pass the env vars prefixed with "SIMCTL_CHILD_" to the app.
+ result = {}
+ for k, v in os.environ.items():
+ if not k.startswith("IOS_"):
+ continue
+ new_key = k.replace("IOS_", "SIMCTL_CHILD_", 1)
+ result[new_key] = v
+ if "IDE_DISABLED_OS_ACTIVITY_DT_MODE" not in os.environ:
+ # Ensure os_log() mirrors writes to stderr. (lldb and Xcode set this
+ # environment variable as well.)
+ result["SIMCTL_CHILD_OS_ACTIVITY_DT_MODE"] = "enable"
+ return result
+
+
+@contextlib.contextmanager
+def apple_simulator(
+ *,
+ platform_type: str,
+ simctl_path: str,
+ minimum_os: str,
+ sim_device: str,
+ sim_os_version: str,
+) -> AppleSimulatorUDID:
+ """Finds either a temporary or persistent Apple simulator based on args.
+
+ Args:
+ platform_type: The Apple platform type for the given *_application() target.
+ simctl_path: The path to the `simctl` binary.
+ minimum_os: The minimum OS version required by the *_application() target.
+ sim_device: Optional name of the device (e.g. "iPhone 8 Plus").
+ sim_os_version: Optional version of the Apple platform runtime (e.g.
+ "13.2").
+
+ Yields:
+ The UDID of the simulator.
+ """
+ yield persistent_simulator(
+ platform_type=platform_type,
+ simctl_path=simctl_path,
+ minimum_os=minimum_os,
+ sim_device=sim_device,
+ sim_os_version=sim_os_version,
+ )
+
+
+def run_app_in_simulator(
+ *,
+ simulator_udid: str,
+ developer_path: str,
+ simctl_path: str,
+ application_output_path: str,
+ app_name: str,
+) -> None:
+ """Installs and runs an app in the specified simulator.
+
+ Args:
+ simulator_udid: The UDID of the simulator in which to run the app.
+ developer_path: The path to /Applications/Xcode.app/Contents/Developer.
+ simctl_path: The path to the `simctl` binary.
+ application_output_path: Path to the output of an `*_application()`.
+ app_name: The name of the application (e.g. "Foo" for "Foo.app").
+ """
+ boot_simulator(
+ developer_path=developer_path,
+ simctl_path=simctl_path,
+ udid=simulator_udid,
+ )
+ root_dir = os.path.dirname(application_output_path)
+ register_dsyms(root_dir)
+ with extracted_app(application_output_path, app_name) as app_path:
+ logger.info("Will use simulator: %s", simulator_udid)
+ logger.info(
+ "Terminating any existing instances of the app in that simulator..."
+ )
+ # First, quietly kill any existing instances of the app
+ app_bundle_id = bundle_id(app_path)
+ subprocess.run(
+ [simctl_path, "terminate", simulator_udid, app_bundle_id],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ logger.info("Installing...")
+ subprocess.run(
+ [simctl_path, "install", simulator_udid, app_path], check=True
+ )
+ logger.info(
+ "Launching..."
+ )
+ args = [
+ simctl_path,
+ "launch",
+ ]
+ # Append optional launch arguments.
+ args.extend(sys.argv[1:])
+ args.extend([
+ simulator_udid,
+ app_bundle_id,
+ ])
+ subprocess.run(args, env=simctl_launch_environ(), check=True)
+
+
+def main(
+ *,
+ app_name: str,
+ application_output_path: str,
+ minimum_os: str,
+ platform_type: str,
+ sim_device: str,
+ sim_os_version: str,
+):
+ """Main entry point to `bazel run` for *_application() targets.
+
+ Args:
+ app_name: The name of the application (e.g. "Foo" for "Foo.app").
+ application_output_path: Path to the output of an *_application().
+ minimum_os: The minimum OS version required by the *_application() target.
+ platform_type: The Apple platform type for the given *_application() target.
+ sim_device: The name of the device (e.g. "iPhone 8 Plus").
+ sim_os_version: The version of the Apple platform runtime (e.g. "13.2").
+ """
+ xcode_select_result = subprocess.run(
+ ["xcode-select", "-p"],
+ encoding="utf-8",
+ check=True,
+ stdout=subprocess.PIPE,
+ )
+ developer_path = xcode_select_result.stdout.rstrip()
+ simctl_path = os.path.join(developer_path, "usr", "bin", "simctl")
+
+ with apple_simulator(
+ platform_type=platform_type,
+ simctl_path=simctl_path,
+ minimum_os=minimum_os,
+ sim_device=sim_device,
+ sim_os_version=sim_os_version,
+ ) as simulator_udid:
+ run_app_in_simulator(
+ simulator_udid=simulator_udid,
+ developer_path=developer_path,
+ simctl_path=simctl_path,
+ application_output_path=application_output_path,
+ app_name=app_name,
+ )
+
+
+if __name__ == "__main__":
+ main(
+ app_name="Telegram",
+ application_output_path="Telegram/Telegram.ipa",
+ minimum_os="13.0",
+ platform_type="ios",
+ sim_device="iPhone 16 Pro",
+ sim_os_version="26.0",
+ )
diff --git a/scripts/launch_and_debug.sh b/scripts/launch_and_debug.sh
new file mode 100755
index 0000000000..401c299fd4
--- /dev/null
+++ b/scripts/launch_and_debug.sh
@@ -0,0 +1,39 @@
+#!/bin/zsh
+
+set -e
+
+# We don't use bazelisk run because it does a bunch of things we don't want in this case.
+# Instead, we have our own script for launching the simulator and lldb.
+# Ideally we should upstream these changes back to rules_apple since they should be useful for everyone.
+
+echo "Building..."
+./build-input/bazel-8.4.2-darwin-arm64 build Telegram/Telegram --announce_rc --features=swift.use_global_module_cache --verbose_failures --remote_cache_async --jobs=16 --define=buildNumber=10000 --define=telegramVersion=12.2.1 --disk_cache=/Users/ali/telegram-bazel-cache -c dbg --ios_multi_cpus=sim_arm64 --watchos_cpus=arm64_32 --features=swift.enable_batch_mode
+chmod -R 777 ./bazel-bin/Telegram
+
+tmp_file=$(pwd)/bazel-bin/Telegram/pid.txt
+rm ${tmp_file} > /dev/null 2>&1 || true
+touch ${tmp_file}
+cp ./scripts/Telegram ./bazel-bin/Telegram/Telegram
+
+pushd ./bazel-bin
+python3 ./Telegram/Telegram --wait-for-debugger --stdout=$(tty) --stderr=$(tty) > ${tmp_file}
+popd
+
+# Get pid from the tmp_file
+echo "$(cat "${tmp_file}" | awk -F': ' '{print $2}')" > ${tmp_file}
+# Ugly hack to remove the newline from the file
+pid=$(tr -d '\n' < ${tmp_file})
+echo "Launched app's pid: ${pid}"
+
+xcode_path=$(xcode-select -p)
+debugserver_path="${xcode_path}/../SharedFrameworks/LLDB.framework/Versions/A/Resources/debugserver"
+
+# Just for sanity, kill any other debugservers that might be running
+pgrep -lfa Resources/debugserver | awk '{print $1}' | xargs kill -9
+
+# Launch the debugserver. The output of this command will signal the IDE to launch the lldb extension,
+# which is hardcoded to connect to port 6667.
+${debugserver_path} "localhost:6667" --attach ${pid}
+
+# Kill the app when debugging ends, just like in Xcode.
+kill -9 ${pid} > /dev/null 2>&1 || true
\ No newline at end of file
diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift
index fa23c3e2f2..0081e2c6f0 100644
--- a/submodules/AccountContext/Sources/AccountContext.swift
+++ b/submodules/AccountContext/Sources/AccountContext.swift
@@ -1178,6 +1178,7 @@ public enum StarsWithdrawalScreenSubject {
case enterAmount(current: StarsAmount, minValue: StarsAmount, fractionAfterCommission: Int, kind: PaidMessageKind, completion: (Int64) -> Void)
case postSuggestion(channel: EnginePeer, isFromAdmin: Bool, current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void)
case postSuggestionModification(current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void)
+ case starGiftOffer(peer: EnginePeer, gift: StarGift.UniqueGift, completion: (CurrencyAmount, Int32) -> Void)
}
public enum ChannelMembersSearchControllerMode {
@@ -1272,6 +1273,7 @@ public protocol SharedAccountContext: AnyObject {
func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?)
func openChatMessage(_ params: OpenChatMessageParams) -> Bool
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError>
+
func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController?
func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
@@ -1355,54 +1357,34 @@ public protocol SharedAccountContext: AnyObject {
func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController?
func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController
func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void)
-
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
-
func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController
-
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController
-
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
-
func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal)?) -> ViewController
func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController
func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
-
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController
-
func makeCameraScreen(context: AccountContext, mode: CameraScreenMode, cameraHolder: Any?, transitionIn: CameraScreenTransitionIn?, transitionOut: @escaping (Bool) -> CameraScreenTransitionOut?, completion: @escaping (Any, @escaping () -> Void) -> Void, transitionedOut: (() -> Void)?) -> ViewController
-
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
-
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, remainingCount: Int32, completion: @escaping ([MediaEditorScreenResult], MediaEditorTransitionOutExternalState, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
-
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
-
func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
-
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
-
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
-
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
-
func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController
-
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
-
func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController
-
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController
-
func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController
func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, messageId: EngineMessage.Id) -> ViewController
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
-
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
@@ -1421,38 +1403,32 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsIntroScreen(context: AccountContext) -> ViewController
func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController
func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController
- func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController
+ func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]?) -> ViewController
+ func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController
func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController
func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController
- func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController
+ func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController
func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController
-
+ func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController
+ func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController
+ func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController
+ func makeGiftDemoScreen(context: AccountContext) -> ViewController
func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController
-
func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?)
-
func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController
-
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
-
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
-
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?)
-
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController
func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController
-
func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController
-
func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController
-
func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController
-
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController
func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController
-
+
@available(iOS 13.0, *)
func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController
@@ -1471,6 +1447,7 @@ public protocol SharedAccountContext: AnyObject {
func makeNewContactScreen(context: AccountContext, peer: EnginePeer?, phoneNumber: String?, shareViaException: Bool, completion: @escaping (EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData?) -> Void) -> ViewController
func makeLoginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
+ func makePasskeySetupController(context: AccountContext, displaySkip: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
func navigateToCurrentCall()
var hasOngoingCall: ValuePromise { get }
diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift
index a97e750a4f..51a2c3f06a 100644
--- a/submodules/AccountContext/Sources/Premium.swift
+++ b/submodules/AccountContext/Sources/Premium.swift
@@ -144,6 +144,7 @@ public enum StarsPurchasePurpose: Equatable {
case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64)
case buyStarGift(requiredStars: Int64)
case removeOriginalDetailsStarGift(requiredStars: Int64)
+ case starGiftOffer(requiredStars: Int64)
}
public struct PremiumConfiguration {
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
index 1074767824..b6d861d6f2 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift
@@ -170,7 +170,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
if let currentController = currentController {
controller = currentController
} else {
- controller = AuthorizationSequencePhoneEntryController(sharedContext: self.sharedContext, account: self.account, isTestingEnvironment: self.account.testingEnvironment, otherAccountPhoneNumbers: self.otherAccountPhoneNumbers, network: self.account.network, presentationData: self.presentationData, openUrl: { [weak self] url in
+ controller = AuthorizationSequencePhoneEntryController(sharedContext: self.sharedContext, account: self.account, apiId: self.apiId, apiHash: self.apiHash, isTestingEnvironment: self.account.testingEnvironment, otherAccountPhoneNumbers: self.otherAccountPhoneNumbers, network: self.account.network, presentationData: self.presentationData, openUrl: { [weak self] url in
self?.openUrl(url)
}, back: { [weak self] in
guard let strongSelf = self else {
@@ -315,6 +315,48 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
}
})
}
+ controller.loginWithPasskey = { [weak self, weak controller] passkey, syncContacts in
+ guard let self else {
+ return
+ }
+
+ self.actionDisposable.set((authorizeWithPasskey(
+ accountManager: self.sharedContext.accountManager,
+ account: self.account,
+ passkey: passkey,
+ foreignDatacenter: nil,
+ forcedPasswordSetupNotice: { value in
+ guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else {
+ return nil
+ }
+ return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry)
+ },
+ syncContacts: syncContacts
+ )
+ |> deliverOnMainQueue).startStrict(next: { [weak self] result in
+ guard let self else {
+ return
+ }
+ if result.updatedAccount !== self.account {
+ self.account = result.updatedAccount
+ self.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(self.engine))
+ }
+ }, error: { [weak self, weak controller] error in
+ Queue.mainQueue().async {
+ if let strongSelf = self, let controller {
+ let text: String
+ switch error {
+ case .limitExceeded:
+ text = strongSelf.presentationData.strings.Login_CodeFloodError
+ case .generic, .invalidEmailAddress, .codeExpired, .invalidEmailToken, .invalidCode:
+ text = strongSelf.presentationData.strings.Login_UnknownError
+ }
+
+ controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
+ }
+ }
+ }))
+ }
}
controller.updateData(countryCode: countryCode, countryName: nil, number: number)
return controller
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
index 74fd3996ef..54855476a3 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift
@@ -12,8 +12,9 @@ import CountrySelectionUI
import PhoneNumberFormat
import DebugSettingsUI
import MessageUI
+import AuthenticationServices
-public final class AuthorizationSequencePhoneEntryController: ViewController, MFMailComposeViewControllerDelegate {
+public final class AuthorizationSequencePhoneEntryController: ViewController, MFMailComposeViewControllerDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
private var controllerNode: AuthorizationSequencePhoneEntryControllerNode {
return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode
}
@@ -22,6 +23,8 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
private let sharedContext: SharedAccountContext
private var account: UnauthorizedAccount?
+ private let apiId: Int32
+ private let apiHash: String
private let isTestingEnvironment: Bool
private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)])
private let network: Network
@@ -52,6 +55,7 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
}
}
public var loginWithNumber: ((String, Bool) -> Void)?
+ public var loginWithPasskey: ((AuthorizationPasskeyData, Bool) -> Void)?
var accountUpdated: ((UnauthorizedAccount) -> Void)?
weak var confirmationController: PhoneConfirmationController?
@@ -60,9 +64,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
private let hapticFeedback = HapticFeedback()
- public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount?, countriesConfiguration: CountriesConfiguration? = nil, isTestingEnvironment: Bool, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), network: Network, presentationData: PresentationData, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) {
+ public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount?, countriesConfiguration: CountriesConfiguration? = nil, apiId: Int32, apiHash: String, isTestingEnvironment: Bool, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), network: Network, presentationData: PresentationData, openUrl: @escaping (String) -> Void, back: @escaping () -> Void) {
self.sharedContext = sharedContext
self.account = account
+ self.apiId = apiId
+ self.apiHash = apiHash
self.isTestingEnvironment = isTestingEnvironment
self.otherAccountPhoneNumbers = otherAccountPhoneNumbers
self.network = network
@@ -151,6 +157,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
strongSelf.account = account
strongSelf.accountUpdated?(account)
}
+ self.controllerNode.retryPasskey = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.loadAndPresentPasskey(force: true)
+ }
if let (code, name, number) = self.currentData {
self.controllerNode.codeAndNumber = (code, name, number)
@@ -187,6 +199,124 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
} else {
self.controllerNode.updateCountryCode()
}
+
+ self.loadAndPresentPasskey(force: false)
+ }
+
+ private func loadAndPresentPasskey(force: Bool) {
+ if #available(iOS 16.0, *) {
+ Task { @MainActor [weak self] in
+ guard let self, let account = self.account else {
+ return
+ }
+
+ let decodeBase64: (String) -> Data? = { string in
+ var string = string.replacingOccurrences(of: "-", with: "+")
+ .replacingOccurrences(of: "_", with: "/")
+ while string.count % 4 != 0 {
+ string.append("=")
+ }
+ return Data(base64Encoded: string)
+ }
+
+ let engine = TelegramEngineUnauthorized(account: account)
+ let passkeyDataString = await engine.auth.requestPasskeyLoginData(apiId: self.apiId, apiHash: self.apiHash).get()
+ guard let passkeyDataString, let passkeyData = passkeyDataString.data(using: .utf8) else {
+ return
+ }
+ guard let params = try? JSONSerialization.jsonObject(with: passkeyData) as? [String: Any] else {
+ return
+ }
+ guard let pkDict = params["publicKey"] as? [String: Any] else {
+ return
+ }
+ guard let relyingPartyIdentifier = pkDict["rpId"] as? String else {
+ return
+ }
+ guard let challengeBase64 = pkDict["challenge"] as? String else {
+ return
+ }
+ guard let challengeData = decodeBase64(challengeBase64) else {
+ return
+ }
+
+ let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
+ let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: challengeData)
+ let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
+ authController.delegate = self
+ authController.presentationContextProvider = self
+ if force {
+ authController.performRequests()
+ } else {
+ authController.performRequests(options: [.preferImmediatelyAvailableCredentials])
+ }
+ }
+ }
+ }
+
+ public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
+ Task { @MainActor [weak self] in
+ guard let self, let account = self.account else {
+ return
+ }
+
+ let encodeBase64URL: (Data) -> String = { data in
+ var string = data.base64EncodedString()
+ string = string
+ .replacingOccurrences(of: "+", with: "-")
+ .replacingOccurrences(of: "/", with: "_")
+ string = string.replacingOccurrences(of: "=", with: "")
+ return string
+ }
+
+ if #available(iOS 17.0, *) {
+ if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
+ guard let clientData = String(data: credential.rawClientDataJSON, encoding: .utf8) else {
+ return
+ }
+ guard let userHandle = String(data: credential.userID, encoding: .utf8) else {
+ return
+ }
+ let passkey = AuthorizationPasskeyData(
+ id: encodeBase64URL(credential.credentialID),
+ clientData: clientData,
+ authenticatorData: credential.rawAuthenticatorData,
+ signature: credential.signature,
+ userHandle: userHandle
+ )
+ self.loginWithPasskey?(passkey, self.controllerNode.syncContacts)
+
+ /*if let clientData = String(data: credential.rawClientDataJSON, encoding: .utf8), let attestationObject = credential.rawAttestationObject {
+ let passkey = await component.context.engine.auth.requestCreatePasskey(id: encodeBase64URL(credential.credentialID), clientData: clientData, attestationObject: attestationObject).get()
+ if let passkey {
+ if self.passkeysData == nil {
+ self.passkeysData = []
+ self.passkeysData?.insert(passkey, at: 0)
+ }
+ self.state?.updated(transition: .immediate)
+ }
+ }*/
+ let _ = account
+ let _ = credential
+ }
+ }
+ }
+ }
+
+ public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
+ if (error as NSError).domain == "com.apple.AuthenticationServices.AuthorizationError" && (error as NSError).code == 1001 {
+ self.controllerNode.updateDisplayPasskeyLoginOption()
+ if let validLayout = self.validLayout {
+ self.containerLayoutUpdated(validLayout, transition: .immediate)
+ }
+ }
+ }
+
+ public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
+ guard let windowScene = self.view.window?.windowScene else {
+ preconditionFailure()
+ }
+ return ASPresentationAnchor(windowScene: windowScene)
}
public func updateCountryCode() {
diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
index fafaac93ba..69ca7fc799 100644
--- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
+++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift
@@ -15,6 +15,7 @@ import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
import AuthorizationUtils
import ManagedAnimationNode
+import Markdown
private final class PhoneAndCountryNode: ASDisplayNode {
let strings: PresentationStrings
@@ -312,7 +313,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let managedAnimationNode: ManagedPhoneAnimationNode
private let titleNode: ASTextNode
private let titleActivateAreaNode: AccessibilityAreaNode
- private let noticeNode: ASTextNode
+ private let noticeNode: ImmediateTextNode
private let noticeActivateAreaNode: AccessibilityAreaNode
private let phoneAndCountryNode: PhoneAndCountryNode
private let contactSyncNode: ContactSyncNode
@@ -323,6 +324,8 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
private let tokenEventsDisposable = MetaDisposable()
var accountUpdated: ((UnauthorizedAccount) -> Void)?
+ var retryPasskey: (() -> Void)?
+
private let debugAction: () -> Void
var currentNumber: String {
@@ -405,7 +408,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.titleActivateAreaNode = AccessibilityAreaNode()
self.titleActivateAreaNode.accessibilityTraits = .staticText
- self.noticeNode = ASTextNode()
+ self.noticeNode = ImmediateTextNode()
self.noticeNode.maximumNumberOfLines = 0
self.noticeNode.isUserInteractionEnabled = true
self.noticeNode.displaysAsynchronously = false
@@ -443,6 +446,23 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
self.addSubnode(self.managedAnimationNode)
self.contactSyncNode.isHidden = true
+ self.noticeNode.highlightAttributeAction = { attributes in
+ if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
+ return NSAttributedString.Key(rawValue: "URL")
+ } else {
+ return nil
+ }
+ }
+ self.noticeNode.tapAttributeAction = { [weak self] attributes, _ in
+ guard let self else {
+ return
+ }
+ if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
+ self.retryPasskey?()
+ }
+ }
+ self.noticeNode.linkHighlightColor = theme.list.itemAccentColor.withAlphaComponent(0.2)
+
self.phoneAndCountryNode.selectCountryCode = { [weak self] in
self?.selectCountryCode?()
}
@@ -484,7 +504,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
super.didLoad()
self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:))))
- #if DEBUG
+ #if DEBUG && false
self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:))))
#endif
}
@@ -555,6 +575,27 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
let _ = self.phoneAndCountryNode.processNumberChange(number: self.phoneAndCountryNode.phoneInputNode.number)
}
+ func updateDisplayPasskeyLoginOption() {
+ if self.account == nil {
+ return
+ }
+ let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(self.strings.Login_PhoneWithPasskeySubtitle, attributes: MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(17.0), textColor: self.theme.list.itemPrimaryTextColor),
+ link: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor),
+ linkAttribute: { url in
+ return ("URL", url)
+ }
+ )))
+ let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: self.theme.list.itemAccentColor)
+
+ if let range = attributedText.string.range(of: ">"), let chevronImage {
+ attributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedText.string))
+ }
+
+ self.noticeNode.attributedText = attributedText
+ }
+
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [])
insets.top = layout.statusBarHeight ?? 20.0
@@ -565,7 +606,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0
let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 80.0 : 10.0
- self.titleNode.attributedText = NSAttributedString(string: self.account == nil ? strings.Login_NewNumber : strings.Login_PhoneTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
+ self.titleNode.attributedText = NSAttributedString(string: self.account == nil ? self.strings.Login_NewNumber : self.strings.Login_PhoneTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor)
self.titleActivateAreaNode.accessibilityLabel = self.titleNode.attributedText?.string ?? ""
let inset: CGFloat = 24.0
@@ -576,7 +617,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode {
let noticeInset: CGFloat = self.account == nil ? 32.0 : 0.0
- let noticeSize = self.noticeNode.measure(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude))
+ let noticeSize = self.noticeNode.updateLayout(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude))
let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - inset * 2.0, transition: transition)
let proceedSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight)
diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift
index 1da8fd8434..e970e67688 100644
--- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift
+++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift
@@ -13,14 +13,14 @@ enum BotCheckoutActionButtonState: Equatable {
private let titleFont = Font.semibold(17.0)
-final class BotCheckoutActionButton: HighlightableButtonNode {
+final class BotCheckoutActionButton: HighlightTrackingButtonNode {
static var height: CGFloat = 52.0
private var activeFillColor: UIColor
private var inactiveFillColor: UIColor
private var foregroundColor: UIColor
- private let activeBackgroundNode: ASImageNode
+ private let activeBackgroundNode: ASDisplayNode
private var applePayButton: UIButton?
private let labelNode: TextNode
@@ -29,23 +29,17 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
private var placeholderNode: ShimmerEffectNode?
- private var activeImage: UIImage?
- private var inactiveImage: UIImage?
-
init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) {
self.activeFillColor = activeFillColor
self.inactiveFillColor = inactiveFillColor
self.foregroundColor = foregroundColor
- let diameter: CGFloat = 20.0
- self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
- self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor)
+ let diameter: CGFloat = 52.0
- self.activeBackgroundNode = ASImageNode()
- self.activeBackgroundNode.displaysAsynchronously = false
- self.activeBackgroundNode.displayWithoutProcessing = true
+ self.activeBackgroundNode = ASDisplayNode()
self.activeBackgroundNode.isLayerBacked = true
- self.activeBackgroundNode.image = self.activeImage
+ self.activeBackgroundNode.backgroundColor = activeFillColor
+ self.activeBackgroundNode.cornerRadius = diameter / 2.0
self.labelNode = TextNode()
self.labelNode.displaysAsynchronously = false
@@ -55,6 +49,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
self.addSubnode(self.activeBackgroundNode)
self.addSubnode(self.labelNode)
+
+ self.highligthedChanged = { [weak self] highlighted in
+ guard let self else {
+ return
+ }
+ let transition = ContainedViewLayoutTransition.animated(duration: highlighted ? 0.25 : 0.35, curve: .spring)
+ if highlighted {
+ let highlightedColor = self.activeFillColor.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
+ transition.updateBackgroundColor(node: self.activeBackgroundNode, color: highlightedColor)
+ transition.updateTransformScale(node: self, scale: 1.05)
+ } else {
+ transition.updateBackgroundColor(node: self.activeBackgroundNode, color: self.activeFillColor)
+ transition.updateTransformScale(node: self, scale: 1.0)
+ }
+ }
}
func setState(_ state: BotCheckoutActionButtonState) {
@@ -93,14 +102,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
placeholderNode.removeFromSupernode()
}
- let image = isEnabled ? self.activeImage : self.inactiveImage
- if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image {
- self.activeBackgroundNode.image = image
- self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
- } else {
- self.activeBackgroundNode.image = image
- }
-
+ self.activeBackgroundNode.backgroundColor = isEnabled ? self.activeFillColor : self.inactiveFillColor
+
let makeLayout = TextNode.asyncLayout(self.labelNode)
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let _ = labelApply()
diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift
index 191f9b27c8..e81bf2e2a4 100644
--- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift
@@ -259,31 +259,31 @@ enum BotCheckoutEntry: ItemListNodeEntry {
}
})
case let .paymentMethod(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openPaymentMethod()
})
case let .shippingInfo(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.address(.street1))
})
case let .shippingMethod(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openShippingMethod()
})
case let .nameInfo(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.name)
})
case let .emailInfo(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.email)
})
case let .phoneInfo(_, text, value):
- return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openInfo(.phone)
})
case let .actionPlaceholder(_, shimmeringIndex):
- return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
}, shimmeringIndex: shimmeringIndex)
}
}
@@ -1245,7 +1245,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
- let bottomPanelHorizontalInset: CGFloat = 16.0
+ let bottomPanelHorizontalInset: CGFloat = 30.0
var botName: String?
if let botPeer = self.botPeerValue {
diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift
index 3104afe921..c2c4a9c922 100644
--- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift
+++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift
@@ -270,7 +270,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
- strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: params.width, height: separatorHeight))
diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift
index 745aa4162f..a88ecb09ed 100644
--- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift
+++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift
@@ -141,13 +141,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
let naturalContentHeight: CGFloat
var verticalOffset: CGFloat = 0.0
if item.isFinal {
- naturalContentHeight = 44.0
+ naturalContentHeight = 52.0
} else {
switch neighbors.bottom {
case .otherSection, .none:
- naturalContentHeight = 44.0
+ naturalContentHeight = 52.0
default:
- naturalContentHeight = 34.0
+ naturalContentHeight = 42.0
}
}
if let _ = previousItem as? BotCheckoutHeaderItem {
@@ -209,7 +209,7 @@ class BotCheckoutPriceItemNode: ListViewItemNode {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel))
- strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel))
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift
index dfba43615a..5d89bef1e7 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift
@@ -70,15 +70,17 @@ final class BotPaymentActionItemNode: BotPaymentItemNode {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemAccentColor)
}
- self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0))
- transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel)))
+ let height: CGFloat = 52.0
+
+ self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height))
+ transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel)))
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 32.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
- transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
+ transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
- return 44.0
+ return height
}
@objc func buttonPressed() {
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift
index f41dda6306..0e376a994e 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift
@@ -45,9 +45,9 @@ final class BotPaymentCardInputItemNode: BotPaymentItemNode, STPPaymentCardTextF
self.cardField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
}
- self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 0.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0))
+ self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 4.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0))
- return 44.0
+ return 52.0
}
func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) {
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift
index b482f85fea..6182eafb47 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift
@@ -102,13 +102,15 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode {
}
}
- self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0))
- transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel)))
+ let height: CGFloat = 52.0
+
+ self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height))
+ transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel)))
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
- transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
+ transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
var textInset = leftInset
if !titleSize.width.isZero {
@@ -117,9 +119,9 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode {
textInset = max(measuredInset, textInset)
let textSize = self.textNode.measure(CGSize(width: width - measuredInset - 8.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
- transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 11.0), size: textSize))
+ transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 16.0), size: textSize))
- return 44.0
+ return height
}
@objc func buttonPressed() {
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift
index 41150436ee..eb3404884c 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift
@@ -108,11 +108,12 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
self.textField.textField.tintColor = theme.list.itemAccentColor
}
+ let height: CGFloat = 52.0
let leftInset: CGFloat = 16.0
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
- transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
+ transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
var textInset = leftInset
if !titleSize.width.isZero {
@@ -121,9 +122,9 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
textInset = max(measuredInset, textInset)
- transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
+ transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 5.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0)))
- return 44.0
+ return height
}
func activateInput() {
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift
index 38d57cefd4..15a03e7c97 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift
@@ -74,7 +74,7 @@ class BotPaymentItemNode: ASDisplayNode {
self.bottomSeparatorNode.isHidden = hasCorners
}
- self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height)))
transition.updateFrame(node: self.maskNode, frame: self.backgroundNode.frame.insetBy(dx: sideInset, dy: 0.0))
diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift
index 5f582bfb9c..cffd44d2e9 100644
--- a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift
@@ -74,14 +74,14 @@ final class BotPaymentSwitchItemNode: BotPaymentItemNode {
let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
- transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize))
+ transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize))
let switchSize = self.switchNode.measure(CGSize(width: 100.0, height: 100.0))
- let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 6.0), size: switchSize)
+ let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 12.0), size: switchSize)
transition.updateFrame(node: self.switchNode, frame: switchFrame)
transition.updateFrame(node: self.buttonNode, frame: switchFrame)
- return 44.0
+ return 52.0
}
@objc private func buttonPressed() {
diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift
index 6100325d62..1d464bdcdf 100644
--- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift
+++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift
@@ -335,7 +335,7 @@ final class BotReceiptControllerNode: ItemListControllerNode {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
- let bottomPanelHorizontalInset: CGFloat = 16.0
+ let bottomPanelHorizontalInset: CGFloat = 30.0
let bottomPanelVerticalInset: CGFloat = 16.0
let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height
diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift
index 8fc6c4d6ce..20a3dbc59a 100644
--- a/submodules/BrowserUI/Sources/BrowserWebContent.swift
+++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift
@@ -1425,9 +1425,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
var nodeList = document.getElementsByTagName('link');
for (var i = 0; i < nodeList.length; i++)
{
- if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon')))
- {
- const node = nodeList[i];
+ var rel = nodeList[i].getAttribute('rel') || '';
+ if (
+ rel === 'icon' ||
+ rel === 'shortcut icon' ||
+ rel.indexOf('apple-touch-icon') === 0
+ ) {
+ var node = nodeList[i];
favicons.push({
url: node.getAttribute('href'),
sizes: node.getAttribute('sizes')
diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift
index 5c5e8f23e3..1dae5215da 100644
--- a/submodules/ChatListUI/Sources/ChatListController.swift
+++ b/submodules/ChatListUI/Sources/ChatListController.swift
@@ -137,6 +137,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let dismissAutoarchiveDisposable = MetaDisposable()
private var didSuggestAutoarchive = false
private var didSuggestLoginEmailSetup = false
+ private var didSuggestLoginPasskeySetup = false
private var presentationData: PresentationData
private let presentationDataValue = Promise()
@@ -2586,6 +2587,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
+ if values.contains(.setupPasskey) {
+ if strongSelf.didSuggestLoginPasskeySetup {
+ return
+ }
+ strongSelf.didSuggestLoginPasskeySetup = true
+
+ let _ = (context.engine.notices.getServerProvidedSuggestions(reload: true)
+ |> deliverOnMainQueue).start(next: { [weak strongSelf] currentValues in
+ guard let strongSelf, currentValues.contains(.setupPasskey) else {
+ return
+ }
+ if let navigationController = strongSelf.navigationController as? NavigationController {
+ let controller = strongSelf.context.sharedContext.makePasskeySetupController(context: strongSelf.context, displaySkip: true, navigationController: navigationController, completion: {
+ let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupPasskey.id).startStandalone()
+ }, dismiss: {
+ let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupPasskey.id).startStandalone()
+ })
+ navigationController.pushViewController(controller)
+ }
+ })
+
+ return
+ }
+
if strongSelf.didSuggestAutoarchive {
return
}
diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift
index 48159bcd2c..4dcd0401d2 100644
--- a/submodules/ComponentFlow/Source/Base/Transition.swift
+++ b/submodules/ComponentFlow/Source/Base/Transition.swift
@@ -58,6 +58,19 @@ private extension ComponentTransition.Animation.Curve {
preconditionFailure()
}
}
+
+ var viewAnimationOptions: UIView.AnimationOptions {
+ switch self {
+ case .linear:
+ return [.curveLinear]
+ case .easeInOut:
+ return [.curveEaseInOut]
+ case .spring:
+ return UIView.AnimationOptions(rawValue: 7 << 16)
+ case .custom:
+ return []
+ }
+ }
}
public extension ComponentTransition.Animation {
@@ -70,6 +83,24 @@ public extension ComponentTransition.Animation {
}
}
+public extension ComponentTransition {
+ func animateView(allowUserInteraction: Bool = false, delay: Double = 0.0, _ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
+ switch self.animation {
+ case .none:
+ f()
+ completion?(true)
+ case let .curve(duration, curve):
+ var options = curve.viewAnimationOptions
+ if allowUserInteraction {
+ options.insert(.allowUserInteraction)
+ }
+ UIView.animate(withDuration: duration, delay: delay, options: options, animations: {
+ f()
+ }, completion: completion)
+ }
+ }
+}
+
public struct ComponentTransition {
public enum Animation {
public enum Curve {
diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift
index 94b2a3a2ba..2979b04e82 100644
--- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift
+++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift
@@ -157,40 +157,6 @@ public final class SheetComponent: C
}
}
- final class BackgroundView: UIView {
- let topCornersView = UIView()
- let bottomCornersView = UIView()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- self.topCornersView.clipsToBounds = true
- self.topCornersView.layer.cornerCurve = .continuous
- self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
-
- self.bottomCornersView.clipsToBounds = true
- self.bottomCornersView.layer.cornerCurve = .continuous
- self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
-
- self.addSubview(self.topCornersView)
- self.topCornersView.addSubview(self.bottomCornersView)
- }
-
- required init?(coder: NSCoder) {
- preconditionFailure()
- }
-
- func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
- transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
- transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
-
- transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
- transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
-
- transition.setBackgroundColor(view: self.bottomCornersView, color: color)
- }
- }
-
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView {
public final class Tag {
public init() {
@@ -208,9 +174,9 @@ public final class SheetComponent: C
private let dimView: UIView
private let scrollView: ScrollView
- private let backgroundView: BackgroundView
+ private let backgroundView: SheetBackgroundView
private var effectView: UIVisualEffectView?
- private let clipView: BackgroundView
+ private let clipView: SheetBackgroundView
private let contentView: ComponentView
private var headerView: ComponentView?
@@ -233,8 +199,8 @@ public final class SheetComponent: C
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceVertical = true
- self.backgroundView = BackgroundView()
- self.clipView = BackgroundView()
+ self.backgroundView = SheetBackgroundView()
+ self.clipView = SheetBackgroundView()
self.contentView = ComponentView()
@@ -589,3 +555,37 @@ public final class SheetComponent: C
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
+
+public final class SheetBackgroundView: UIView {
+ let topCornersView = UIView()
+ let bottomCornersView = UIView()
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ self.topCornersView.clipsToBounds = true
+ self.topCornersView.layer.cornerCurve = .continuous
+ self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+
+ self.bottomCornersView.clipsToBounds = true
+ self.bottomCornersView.layer.cornerCurve = .continuous
+ self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
+
+ self.addSubview(self.topCornersView)
+ self.topCornersView.addSubview(self.bottomCornersView)
+ }
+
+ required init?(coder: NSCoder) {
+ preconditionFailure()
+ }
+
+ public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
+ transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
+ transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
+
+ transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
+ transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
+
+ transition.setBackgroundColor(view: self.bottomCornersView, color: color)
+ }
+}
diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift
index 1b865e41ea..a6c32595ea 100644
--- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift
+++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift
@@ -371,7 +371,7 @@ class GeneralChartComponentController: ChartThemeContainer {
values.append(ChartDetailsViewModel.Value(
prefix: nil,
title: self.strings.revenueInUsd,
- value: "≈$\(convertedValueString)",
+ value: "~$\(convertedValueString)",
color: color,
visible: firstValue.visible
))
diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift
index 9f1339a6e6..1450e4bae6 100644
--- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift
+++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift
@@ -161,7 +161,7 @@ class BarsComponentController: GeneralChartComponentController {
} else {
text = String(format: "%0.3f", convertedValue)
}
- updatedLabels.append(LinesChartLabel(value: label.value, text: "≈$\(text)"))
+ updatedLabels.append(LinesChartLabel(value: label.value, text: "~$\(text)"))
}
labels = updatedLabels
}
diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
index 2ffe94c99b..13fc11eb8d 100644
--- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
+++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
@@ -494,7 +494,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
var label: String = ""
if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero {
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
- label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
+ label = presentationData.strings.InviteLink_Create_FeePerMonth("~\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string
}
entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0)))
}
diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift
index a4c7ed9ba3..d017ac6395 100644
--- a/submodules/ListMessageItem/Sources/ListMessageItem.swift
+++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift
@@ -44,7 +44,7 @@ public final class ListMessageItemInteraction {
})
}
-public final class ListMessageItem: ListViewItem {
+public final class ListMessageItem: ListViewItem, ItemListItem {
let presentationData: ChatPresentationData
let systemStyle: ItemListSystemStyle
let context: AccountContext
@@ -57,6 +57,7 @@ public final class ListMessageItem: ListViewItem {
let isGlobalSearchResult: Bool
let isDownloadList: Bool
let isSavedMusic: Bool
+ let isStoryMusic: Bool
let displayFileInfo: Bool
let displayBackground: Bool
let canReorder: Bool
@@ -64,9 +65,11 @@ public final class ListMessageItem: ListViewItem {
let header: ListViewItemHeader?
+ public var sectionId: ItemListSectionId
+
public let selectable: Bool = true
- public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) {
+ public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, isStoryMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.context = context
@@ -86,10 +89,12 @@ public final class ListMessageItem: ListViewItem {
self.isGlobalSearchResult = isGlobalSearchResult
self.isDownloadList = isDownloadList
self.isSavedMusic = isSavedMusic
+ self.isStoryMusic = isStoryMusic
self.displayFileInfo = displayFileInfo
self.displayBackground = displayBackground
self.canReorder = canReorder
self.style = style
+ self.sectionId = sectionId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) {
@@ -120,7 +125,23 @@ public final class ListMessageItem: ListViewItem {
node.setupItem(self)
let nodeLayout = node.asyncLayout()
- let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
+
+ var topMerged = false
+ if let previousItem {
+ if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain {
+ topMerged = true
+ }
+ }
+
+ var bottomMerged = false
+ if let nextItem {
+ if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain {
+ bottomMerged = true
+ }
+ }
+
+
+ let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
node.updateSelectionState(animated: false)
@@ -152,9 +173,22 @@ public final class ListMessageItem: ListViewItem {
let nodeLayout = nodeValue.asyncLayout()
+ var topMerged = false
+ if let previousItem {
+ if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain {
+ topMerged = true
+ }
+ }
+
+ var bottomMerged = false
+ if let nextItem {
+ if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain {
+ bottomMerged = true
+ }
+ }
+
async {
- let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem))
-
+ let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom)
Queue.mainQueue().async {
completion(layout, { _ in
diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift
index 434b96002e..9942427e0e 100644
--- a/submodules/LocationUI/Sources/LocationPickerController.swift
+++ b/submodules/LocationUI/Sources/LocationPickerController.swift
@@ -446,9 +446,9 @@ public func storyLocationPickerController(
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData))
- let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
+ let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
controller.requestController = { _, present in
- let locationPickerController = LocationPickerController(context: context, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in
+ let locationPickerController = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in
completion(location, queryId, resultId, address, countryCode)
})
present(locationPickerController, locationPickerController.mediaPickerContext)
diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift
index e57222dfb6..dfe78fa1cc 100644
--- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift
+++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift
@@ -19,7 +19,7 @@ public func mediaPasteboardScreen(
getSourceRect: (() -> CGRect?)? = nil,
makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil }
) -> ViewController {
- let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView)
+ let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView)
controller.requestController = { _, present in
presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in
present(mediaPicker, mediaPickerContext)
diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD
index 768e2b7bd2..ff18627371 100644
--- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD
+++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD
@@ -26,6 +26,9 @@ swift_library(
"//submodules/Components/ActivityIndicatorComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
+ "//submodules/TelegramUI/Components/ListSectionComponent",
+ "//submodules/TelegramUI/Components/ListActionItemComponent",
+ "//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift
index 939d80ae68..999385a0d6 100644
--- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift
+++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift
@@ -14,6 +14,9 @@ import BundleIconComponent
import AnimatedStickerComponent
import ActivityIndicatorComponent
import GlassBarButtonComponent
+import ListSectionComponent
+import ListActionItemComponent
+import PlainButtonComponent
private final class CreateExternalMediaStreamScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -49,41 +52,51 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
final class State: ComponentState {
let context: AccountContext
let peerId: EnginePeer.Id
+ let mode: CreateExternalMediaStreamScreen.Mode
private(set) var credentials: GroupCallStreamCredentials?
var isDelayingLoadingIndication: Bool = true
- private var credentialsDisposable: Disposable?
+ private let credentialsDisposable = MetaDisposable()
private let activeActionDisposable = MetaDisposable()
- init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?) {
+ init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) {
self.context = context
self.peerId = peerId
+ self.mode = mode
super.init()
+ self.getCredentials(credentialsPromise: credentialsPromise)
+ }
+
+ deinit {
+ self.credentialsDisposable.dispose()
+ self.activeActionDisposable.dispose()
+ }
+
+ func getCredentials(credentialsPromise: Promise? = nil, revoke: Bool = false) {
let credentialsSignal: Signal
if let credentialsPromise = credentialsPromise {
credentialsSignal = credentialsPromise.get()
} else {
- credentialsSignal = context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, isLiveStream: false, revokePreviousCredentials: false)
+ var isLiveStream = false
+ if case let .create(isLiveStreamValue) = self.mode {
+ isLiveStream = isLiveStreamValue
+ }
+ credentialsSignal = self.context.engine.calls.getGroupCallStreamCredentials(peerId: self.peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revoke)
|> `catch` { _ -> Signal in
return .never()
}
}
- self.credentialsDisposable = (credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in
+ self.credentialsDisposable.set((credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.credentials = result
strongSelf.updated(transition: .immediate)
- })
- }
-
- deinit {
- self.credentialsDisposable?.dispose()
- self.activeActionDisposable.dispose()
+ }))
}
func copyCredentials(_ key: KeyPath) {
@@ -160,7 +173,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
}
func makeState() -> State {
- return State(context: self.context, peerId: self.peerId, credentialsPromise: self.credentialsPromise)
+ return State(context: self.context, peerId: self.peerId, mode: self.mode, credentialsPromise: self.credentialsPromise)
}
static var body: Body {
@@ -176,26 +189,25 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let activityIndicator = Child(ActivityIndicatorComponent.self)
- let credentialsBackground = Child(RoundedRectangle.self)
+ let credentialsSection = Child(ListSectionComponent.self)
- let credentialsStripe = Child(Rectangle.self)
- let credentialsURLTitle = Child(MultilineTextComponent.self)
- let credentialsURLText = Child(MultilineTextComponent.self)
-
- let credentialsKeyTitle = Child(MultilineTextComponent.self)
- let credentialsKeyText = Child(MultilineTextComponent.self)
-
- let credentialsCopyURLButton = Child(Button.self)
- let credentialsCopyKeyButton = Child(Button.self)
+// let credentialsBackground = Child(RoundedRectangle.self)
+// let credentialsStripe = Child(Rectangle.self)
+// let credentialsURLTitle = Child(MultilineTextComponent.self)
+// let credentialsURLText = Child(MultilineTextComponent.self)
+//
+// let credentialsKeyTitle = Child(MultilineTextComponent.self)
+// let credentialsKeyText = Child(MultilineTextComponent.self)
+//
+// let credentialsCopyURLButton = Child(Button.self)
+// let credentialsCopyKeyButton = Child(Button.self)
return { context in
let topInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0
let buttonSideInset: CGFloat = 36.0
- let credentialsSideInset: CGFloat = 16.0
- let credentialsTopInset: CGFloat = 12.0
- let credentialsTitleSpacing: CGFloat = 5.0
+ let component = context.component
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
@@ -355,124 +367,145 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
)
let textFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - text.size.width) / 2.0), y: animationFrame.maxY + 18.0), size: text.size)
-
context.add(text
.position(CGPoint(x: textFrame.midX, y: textFrame.midY))
)
- let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize)
-
if let credentials = context.state.credentials {
- let credentialsURLTitle = credentialsURLTitle.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
- horizontalAlignment: .left,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
- transition: context.transition
+ var credentialsSectionItems: [AnyComponentWithIdentity] = []
+ credentialsSectionItems.append(
+ AnyComponentWithIdentity(id: "url", component: AnyComponent(
+ ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.CreateExternalStream_ServerUrl,
+ font: Font.regular(15.0),
+ textColor: theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
+ horizontalAlignment: .left,
+ truncationType: .middle,
+ maximumNumberOfLines: 1
+ )))
+ ], alignment: .left, spacing: 5.0)),
+ contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0),
+ accessory: .custom(ListActionItemComponent.CustomAccessory(
+ component: AnyComponentWithIdentity(
+ id: "copy",
+ component: AnyComponent(
+ PlainButtonComponent(
+ content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
+ action: { [weak state] in
+ guard let state = state else {
+ return
+ }
+ state.copyCredentials(\.url)
+ },
+ animateScale: false
+ )
+ )
+ ),
+ insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
+ isInteractive: true
+ )),
+ action: nil
+ )
+ ))
)
- let credentialsKeyTitle = credentialsKeyTitle.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)),
- horizontalAlignment: .left,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
- transition: context.transition
+ credentialsSectionItems.append(
+ AnyComponentWithIdentity(id: "key", component: AnyComponent(
+ ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.CreateExternalStream_StreamKey,
+ font: Font.regular(15.0),
+ textColor: theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
+ horizontalAlignment: .left,
+ truncationType: .middle,
+ maximumNumberOfLines: 1
+ )))
+ ], alignment: .left, spacing: 5.0)),
+ contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0),
+ accessory: .custom(ListActionItemComponent.CustomAccessory(
+ component: AnyComponentWithIdentity(
+ id: "copy",
+ component: AnyComponent(
+ PlainButtonComponent(
+ content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
+ action: { [weak state] in
+ guard let state = state else {
+ return
+ }
+ state.copyCredentials(\.streamKey)
+ },
+ animateScale: false
+ )
+ )
+ ),
+ insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
+ isInteractive: true
+ )),
+ action: nil
+ )
+ ))
)
- let credentialsURLText = credentialsURLText.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
- horizontalAlignment: .left,
- truncationType: .middle,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height),
- transition: context.transition
- )
-
- let credentialsKeyText = credentialsKeyText.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)),
- horizontalAlignment: .left,
- truncationType: .middle,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 48.0, height: credentialsAreaSize.height),
- transition: context.transition
- )
-
- let credentialsBackground = credentialsBackground.update(
- component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0),
- availableSize: credentialsAreaSize,
- transition: context.transition
- )
-
- let credentialsStripe = credentialsStripe.update(
- component: Rectangle(color: theme.list.itemPlainSeparatorColor),
- availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: UIScreenPixel),
- transition: context.transition
- )
-
- let credentialsCopyURLButton = credentialsCopyURLButton.update(
- component: Button(
- content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
- action: { [weak state] in
- guard let state = state else {
- return
+ credentialsSectionItems.append(
+ AnyComponentWithIdentity(id: "revoke", component: AnyComponent(
+ ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_RevokeStreamKey, font: Font.regular(17.0), textColor: theme.list.itemDestructiveColor)),
+ horizontalAlignment: .center,
+ truncationType: .middle,
+ maximumNumberOfLines: 1
+ )),
+ titleAlignment: .center,
+ action: { [weak state] _ in
+ guard let state = state else {
+ return
+ }
+ let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {
+ }), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in
+ state?.getCredentials(revoke: true)
+ })])
+ environment.controller()?.present(alertController, in: .window(.root))
}
- state.copyCredentials(\.url)
- }
- ).minSize(CGSize(width: 44.0, height: 44.0)),
- availableSize: CGSize(width: 44.0, height: 44.0),
+ )
+ ))
+ )
+
+ let credentialsSection = credentialsSection.update(
+ component: ListSectionComponent(
+ theme: theme,
+ style: .glass,
+ header: nil,
+ footer: nil,
+ items: credentialsSectionItems
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
-
- let credentialsCopyKeyButton = credentialsCopyKeyButton.update(
- component: Button(
- content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)),
- action: { [weak state] in
- guard let state = state else {
- return
- }
- state.copyCredentials(\.streamKey)
- }
- ).minSize(CGSize(width: 44.0, height: 44.0)),
- availableSize: CGSize(width: 44.0, height: 44.0),
- transition: context.transition
- )
-
- context.add(credentialsBackground
- .position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY))
- )
-
- context.add(credentialsStripe
- .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsStripe.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight))
- )
-
- context.add(credentialsURLTitle
- .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsURLTitle.size.height / 2.0))
- )
- context.add(credentialsURLText
- .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLText.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsTitleSpacing + credentialsURLTitle.size.height + credentialsURLText.size.height / 2.0))
- )
- context.add(credentialsCopyURLButton
- .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyURLButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight / 2.0))
- )
-
- context.add(credentialsKeyTitle
- .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsKeyTitle.size.height / 2.0))
- )
- context.add(credentialsKeyText
- .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyText.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsTitleSpacing + credentialsKeyTitle.size.height + credentialsKeyText.size.height / 2.0))
- )
- context.add(credentialsCopyKeyButton
- .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyKeyButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsItemHeight / 2.0))
- )
+ context.add(credentialsSection
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: textFrame.maxY + 30.0 + credentialsSection.size.height / 2.0)))
} else if !context.state.isDelayingLoadingIndication {
+ let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize)
let activityIndicator = activityIndicator.update(
component: ActivityIndicatorComponent(color: theme.list.controlSecondaryColor),
availableSize: CGSize(width: 100.0, height: 100.0),
diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift
index d4f2c23ef0..deb1d436b9 100644
--- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift
+++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift
@@ -548,34 +548,6 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm
}
}
-private func rightDependencies(_ right: TelegramChatAdminRightsFlags) -> [TelegramChatAdminRightsFlags] {
- if right.contains(.canChangeInfo) {
- return []
- } else if right.contains(.canPostMessages) {
- return []
- } else if right.contains(.canEditMessages) {
- return []
- } else if right.contains(.canDeleteMessages) {
- return []
- } else if right.contains(.canBanUsers) {
- return []
- } else if right.contains(.canInviteUsers) {
- return []
- } else if right.contains(.canPinMessages) {
- return []
- } else if right.contains(.canAddAdmins) {
- return []
- } else if right.contains(.canManageDirect) {
- return []
- } else if right.contains(.canManageCalls) {
- return []
- } else if right.contains(.canBeAnonymous) {
- return []
- } else {
- return []
- }
-}
-
private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: EnginePeer, initialParticipant: ChannelParticipant?) -> Bool {
if case let .channel(channel) = channelPeer {
if channel.flags.contains(.isCreator) {
@@ -658,6 +630,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
.direct(.canChangeInfo),
.sub(.messages, messageRelatedFlags),
.sub(.stories, storiesRelatedFlags),
+ .direct(.canBanUsers),
.direct(.canInviteUsers),
.direct(.canManageDirect),
.direct(.canManageCalls),
diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift
index df917543dc..5e130739d4 100644
--- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift
+++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift
@@ -732,7 +732,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen
var price: String = ""
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
- price = "≈\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
+ price = "~\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice))
entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price))
diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift
index 7ad7acf566..9fa9fc9528 100644
--- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift
+++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift
@@ -354,15 +354,15 @@ protocol PhoneDemoDecorationView: UIView {
func resetAnimation()
}
-final class PhoneDemoComponent: Component {
- typealias EnvironmentType = DemoPageEnvironment
+public final class PhoneDemoComponent: Component {
+ public typealias EnvironmentType = DemoPageEnvironment
- enum Position {
+ public enum Position {
case top
case bottom
}
- enum BackgroundDecoration {
+ public enum BackgroundDecoration {
case none
case dataRain
case swirlStars
@@ -375,7 +375,7 @@ final class PhoneDemoComponent: Component {
case todo
}
- enum Model {
+ public enum Model {
case notch
case island
}
@@ -419,11 +419,11 @@ final class PhoneDemoComponent: Component {
return true
}
- final class View: UIView, ComponentTaggedView {
- final class Tag {
+ public final class View: UIView, ComponentTaggedView {
+ public final class Tag {
}
- func matches(tag: Any) -> Bool {
+ public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag, self.isCentral {
return true
}
diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift
index 9ff8e1b6b8..e531a8b126 100644
--- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift
+++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift
@@ -155,7 +155,7 @@ public final class PremiumGradientBackgroundComponent: Component {
}
}
-final class DemoPageEnvironment: Equatable {
+public final class DemoPageEnvironment: Equatable {
public let isDisplaying: Bool
public let isCentral: Bool
public let position: CGFloat
@@ -1134,13 +1134,13 @@ private final class DemoSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
- backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
- isDark: theme.overallDarkAppearance,
- state: .glass,
+ backgroundColor: UIColor(rgb: 0x7f76f4),
+ isDark: false,
+ state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
- tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
+ tintColor: .white
)
)),
action: { _ in
diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
index 1327b3be18..71f3a6e4e6 100644
--- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
+++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift
@@ -2049,7 +2049,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
subtitle = "\(environment.strings.Gift_Options_Premium_Months(product.months)) • \(product.price)"
} else {
- subtitle = product.price
+ //subtitle = product.price
+ subtitle = "\(environment.strings.Gift_Options_Premium_Months(product.months)) • \(product.price)"
}
}
if product.isCurrent {
diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD
index abcd3e572f..353a1ec8ee 100644
--- a/submodules/SettingsUI/BUILD
+++ b/submodules/SettingsUI/BUILD
@@ -126,6 +126,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
"//submodules/TelegramUI/Components/Settings/GenerateThemeName",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
+ "//submodules/TelegramUI/Components/Settings/PasskeysScreen",
"//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/ComponentFlow",
"//submodules/Components/BundleIconComponent",
diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift
index 9b6dd2d5c2..bda486bba9 100644
--- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift
+++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift
@@ -22,7 +22,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
let requestDisposable = MetaDisposable()
let changePhoneDisposable = MetaDisposable()
- let controller = AuthorizationSequencePhoneEntryController(sharedContext: context.sharedContext, account: nil, countriesConfiguration: context.currentCountriesConfiguration.with { $0 }, isTestingEnvironment: false, otherAccountPhoneNumbers: (nil, []), network: context.account.network, presentationData: presentationData, openUrl: { _ in }, back: {
+ let controller = AuthorizationSequencePhoneEntryController(sharedContext: context.sharedContext, account: nil, countriesConfiguration: context.currentCountriesConfiguration.with { $0 }, apiId: 0, apiHash: "", isTestingEnvironment: false, otherAccountPhoneNumbers: (nil, []), network: context.account.network, presentationData: presentationData, openUrl: { _ in }, back: {
dismissImpl?()
})
controller.loginWithNumber = { [weak controller] phoneNumber, _ in
diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift
index c99013d06d..52ec1c3c80 100644
--- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift
+++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift
@@ -194,7 +194,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD
let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0
- let price = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
+ let price = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium))
entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price))
diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
index 11fbbb0e8c..09aeb82886 100644
--- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
+++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift
@@ -19,6 +19,7 @@ import PremiumUI
import AuthorizationUI
import AuthenticationServices
import ChatTimerScreen
+import PasskeysScreen
private final class PrivacyAndSecurityControllerArguments {
let account: Account
@@ -35,6 +36,7 @@ private final class PrivacyAndSecurityControllerArguments {
let openSavedMusicPrivacy: () -> Void
let openPasscode: () -> Void
let openTwoStepVerification: (TwoStepVerificationAccessConfiguration?) -> Void
+ let openPasskeys: () -> Void
let openActiveSessions: () -> Void
let toggleArchiveAndMuteNonContacts: (Bool) -> Void
let setupAccountAutoremove: () -> Void
@@ -44,7 +46,7 @@ private final class PrivacyAndSecurityControllerArguments {
let openMessagePrivacy: () -> Void
let openGiftsPrivacy: () -> Void
- init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openBioPrivacy: @escaping () -> Void, openBirthdayPrivacy: @escaping () -> Void, openSavedMusicPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void, openMessagePrivacy: @escaping () -> Void, openGiftsPrivacy: @escaping () -> Void) {
+ init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openBioPrivacy: @escaping () -> Void, openBirthdayPrivacy: @escaping () -> Void, openSavedMusicPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openPasskeys: @escaping () -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void, openMessagePrivacy: @escaping () -> Void, openGiftsPrivacy: @escaping () -> Void) {
self.account = account
self.openBlockedUsers = openBlockedUsers
self.openLastSeenPrivacy = openLastSeenPrivacy
@@ -59,6 +61,7 @@ private final class PrivacyAndSecurityControllerArguments {
self.openSavedMusicPrivacy = openSavedMusicPrivacy
self.openPasscode = openPasscode
self.openTwoStepVerification = openTwoStepVerification
+ self.openPasskeys = openPasskeys
self.openActiveSessions = openActiveSessions
self.toggleArchiveAndMuteNonContacts = toggleArchiveAndMuteNonContacts
self.setupAccountAutoremove = setupAccountAutoremove
@@ -113,6 +116,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case selectivePrivacyInfo(PresentationTheme, String)
case passcode(PresentationTheme, String, Bool, String)
case twoStepVerification(PresentationTheme, String, String, TwoStepVerificationAccessConfiguration?)
+ case passkeys(PresentationTheme, String, String)
case loginEmail(PresentationTheme, String, String?)
case loginEmailInfo(PresentationTheme, String)
case activeSessions(PresentationTheme, String, String)
@@ -129,7 +133,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
- case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .messageAutoremoveTimeout, .messageAutoremoveInfo:
+ case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .passkeys, .messageAutoremoveTimeout, .messageAutoremoveInfo:
return PrivacyAndSecuritySection.general.rawValue
case .loginEmail, .loginEmailInfo:
return PrivacyAndSecuritySection.loginEmail.rawValue
@@ -154,60 +158,62 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
return 3
case .twoStepVerification:
return 4
- case .messageAutoremoveTimeout:
+ case .passkeys:
return 5
- case .messageAutoremoveInfo:
+ case .messageAutoremoveTimeout:
return 6
- case .loginEmail:
+ case .messageAutoremoveInfo:
return 7
- case .loginEmailInfo:
+ case .loginEmail:
return 8
- case .privacyHeader:
+ case .loginEmailInfo:
return 9
- case .phoneNumberPrivacy:
+ case .privacyHeader:
return 10
- case .lastSeenPrivacy:
+ case .phoneNumberPrivacy:
return 11
- case .profilePhotoPrivacy:
+ case .lastSeenPrivacy:
return 12
- case .bioPrivacy:
+ case .profilePhotoPrivacy:
return 13
- case .giftsAutoSavePrivacy:
+ case .bioPrivacy:
return 14
- case .birthdayPrivacy:
+ case .giftsAutoSavePrivacy:
return 15
- case .savedMusicPrivacy:
+ case .birthdayPrivacy:
return 16
- case .forwardPrivacy:
+ case .savedMusicPrivacy:
return 17
- case .voiceCallPrivacy:
+ case .forwardPrivacy:
return 18
- case .voiceMessagePrivacy:
+ case .voiceCallPrivacy:
return 19
- case .messagePrivacy:
+ case .voiceMessagePrivacy:
return 20
- case .groupPrivacy:
+ case .messagePrivacy:
return 21
- case .groupPrivacyFooter:
+ case .groupPrivacy:
return 22
- case .selectivePrivacyInfo:
+ case .groupPrivacyFooter:
return 23
- case .autoArchiveHeader:
+ case .selectivePrivacyInfo:
return 24
- case .autoArchive:
+ case .autoArchiveHeader:
return 25
- case .autoArchiveInfo:
+ case .autoArchive:
return 26
- case .accountHeader:
+ case .autoArchiveInfo:
return 27
- case .accountTimeout:
+ case .accountHeader:
return 28
- case .accountInfo:
+ case .accountTimeout:
return 29
- case .dataSettings:
+ case .accountInfo:
return 30
- case .dataSettingsInfo:
+ case .dataSettings:
return 31
+ case .dataSettingsInfo:
+ return 32
}
}
@@ -321,6 +327,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .passkeys(lhsTheme, lhsText, lhsValue):
+ if case let .passkeys(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
+ return true
+ } else {
+ return false
+ }
case let .loginEmail(lhsTheme, lhsText, lhsEmailPattern):
if case let .loginEmail(rhsTheme, rhsText, rhsEmailPattern) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEmailPattern == rhsEmailPattern {
return true
@@ -488,6 +500,10 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openTwoStepVerification(data)
})
+ case let .passkeys(_, text, value):
+ return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Passkeys")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
+ arguments.openPasskeys()
+ })
case let .messageAutoremoveTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Timer")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupMessageAutoremove()
@@ -617,6 +633,8 @@ private func privacyAndSecurityControllerEntries(
activeWebsitesCount: Int,
hasTwoStepAuth: Bool?,
twoStepAuthData: TwoStepVerificationAccessConfiguration?,
+ hasPasskeys: Bool?,
+ displayPasskeys: Bool,
canAutoarchive: Bool,
isPremiumDisabled: Bool,
isPremium: Bool,
@@ -654,6 +672,14 @@ private func privacyAndSecurityControllerEntries(
}
entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth, twoStepAuthString, twoStepAuthData))
+ if displayPasskeys {
+ var passkeysString = ""
+ if let hasPasskeys = hasPasskeys {
+ passkeysString = hasPasskeys ? presentationData.strings.PrivacySettings_PasscodeOn : presentationData.strings.PrivacySettings_PasscodeOff
+ }
+ entries.append(.passkeys(presentationData.theme, presentationData.strings.PrivacySettings_Passkey, passkeysString))
+ }
+
if let privacySettings = privacySettings {
let value: Int32?
if let updatingMessageAutoremoveTimeoutValue = state.updatingMessageAutoremoveTimeoutValue {
@@ -788,6 +814,7 @@ public func privacyAndSecurityController(
updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil,
updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil,
updatedHasTwoStepAuth: ((Bool) -> Void)? = nil,
+ updatedHasPasskeys: ((Bool) -> Void)? = nil,
focusOnItemTag: PrivacyAndSecurityEntryTag? = nil,
activeSessionsContext: ActiveSessionsContext? = nil,
webSessionsContext: WebSessionsContext? = nil,
@@ -835,6 +862,10 @@ public func privacyAndSecurityController(
let updateTwoStepAuthDisposable = MetaDisposable()
actionsDisposable.add(updateTwoStepAuthDisposable)
+
+ let updatePasskeyDataDisposable = MetaDisposable()
+ actionsDisposable.add(updatePasskeyDataDisposable)
+
let twoStepAuthDataValue = Promise(nil)
let hasTwoStepAuthDataValue = twoStepAuthDataValue.get()
|> mapToSignal { data -> Signal in
@@ -855,6 +886,19 @@ public func privacyAndSecurityController(
} else {
twoStepAuth.set(hasTwoStepAuthDataValue)
}
+
+ let passkeysDataValue = Promise<[TelegramPasskey]?>(nil)
+ let hasPasskeysDataValue = passkeysDataValue.get()
+ |> mapToSignal { data -> Signal in
+ if let data = data {
+ return .single(!data.isEmpty)
+ } else {
+ return .single(nil)
+ }
+ }
+
+ let passkeys = Promise()
+ passkeys.set(hasPasskeysDataValue)
let loginEmail: Signal
if let loginEmailPattern = loginEmailPattern {
@@ -893,6 +937,23 @@ public func privacyAndSecurityController(
)
}
updateHasTwoStepAuth()
+
+ let updateHasPasskeys: () -> Void = {
+ let signal = context.engine.auth.passkeysData()
+ |> map { value -> [TelegramPasskey]? in
+ return value
+ }
+ |> deliverOnMainQueue
+ updatePasskeyDataDisposable.set(
+ signal.start(next: { value in
+ passkeysDataValue.set(.single(value))
+ if let value {
+ updatedHasPasskeys?(!value.isEmpty)
+ }
+ })
+ )
+ }
+ updateHasPasskeys()
var setupEmailImpl: ((String?) -> Void)?
@@ -1203,6 +1264,14 @@ public func privacyAndSecurityController(
let controller = twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: data.flatMap({ Signal.single(.access(configuration: $0)) })))
pushControllerImpl?(controller, true)
})
+ }, openPasskeys: {
+ Task { @MainActor in
+ let initialPasskeysData = await (passkeysDataValue.get() |> take(1)).get()
+ let passkeysScreen = PasskeysScreen(context: context, displaySkip: false, initialPasskeysData: initialPasskeysData, passkeysDataUpdated: { passkeysData in
+ passkeysDataValue.set(.single(passkeysData))
+ }, completion: {}, cancel: {})
+ pushControllerImpl?(passkeysScreen, true)
+ }
}, openActiveSessions: {
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: true), true)
}, toggleArchiveAndMuteNonContacts: { archiveValue in
@@ -1442,6 +1511,15 @@ public func privacyAndSecurityController(
updatedBlockedPeers?(blockedPeersContext)
}))
+ var displayPasskeys = false
+ if let data = context.currentAppConfiguration.with({ $0 }).data {
+ if let _ = data["ios_display_passkeys"] {
+ displayPasskeys = true
+ } else if let isDev = data["dev"] as? Double, isDev == 1.0 {
+ displayPasskeys = true
+ }
+ }
+
let signal = combineLatest(
queue: .mainQueue(),
context.sharedContext.presentationData,
@@ -1454,11 +1532,12 @@ public func privacyAndSecurityController(
webSessionsContext.state,
context.sharedContext.accountManager.accessChallengeData(),
combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get()),
+ combineLatest(passkeys.get(), passkeysDataValue.get()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
loginEmail
)
- |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth, appConfiguration, accountPeer, loginEmail -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth, passkeys, appConfiguration, accountPeer, loginEmail -> (ItemListControllerState, (ItemListNodeState, Any)) in
var canAutoarchive = false
if let data = appConfiguration.data, let hasAutoarchive = data["autoarchive_setting_available"] as? Bool {
canAutoarchive = hasAutoarchive
@@ -1474,7 +1553,7 @@ public func privacyAndSecurityController(
let isPremium = accountPeer?.isPremium ?? false
let isPremiumDisabled = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }).isPremiumDisabled
- let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail, accountPeer: accountPeer), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, hasPasskeys: passkeys.0, displayPasskeys: displayPasskeys, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail, accountPeer: accountPeer), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}
diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift
index 3c9d2c4f20..98441dc464 100644
--- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift
+++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift
@@ -180,10 +180,10 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode {
case .ton:
let cryptoValue = formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat)
amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
- value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
+ value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
case .stars:
amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance.amount, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
- value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
+ value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))"
isStars = true
}
} else {
diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift
index 4a707b0696..a5b9d6d2d2 100644
--- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift
+++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift
@@ -443,7 +443,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
- item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–",
+ item.publicShares.flatMap { "~\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
.generic
@@ -485,7 +485,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
- item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–",
+ item.publicShares.flatMap { "~\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
.generic
@@ -512,7 +512,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.context,
params.width,
item.presentationData,
- "≈\(Int(stats.premiumAudience?.value ?? 0))",
+ "~\(Int(stats.premiumAudience?.value ?? 0))",
item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic),
.generic
@@ -770,7 +770,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Available,
- (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -780,7 +780,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Current,
- (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -790,7 +790,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Total,
- (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -800,7 +800,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
- (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@@ -810,7 +810,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
- (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@@ -820,7 +820,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(additionalStats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
" ",
- (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@@ -834,7 +834,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Available,
- (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -844,7 +844,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Current,
- (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -854,7 +854,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_Overview_Total,
- (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.ton
)
@@ -869,7 +869,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Available,
- (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@@ -879,7 +879,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Current,
- (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
@@ -889,7 +889,7 @@ class StatsOverviewItemNode: ListViewItemNode {
item.presentationData,
formatStarsAmountText(stats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat),
item.presentationData.strings.Monetization_StarsProceeds_Total,
- (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
+ (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic),
.stars
)
diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift
index 52a36691d7..1f0fce870e 100644
--- a/submodules/TelegramApi/Sources/Api0.swift
+++ b/submodules/TelegramApi/Sources/Api0.swift
@@ -432,6 +432,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1548122514] = { return Api.InputNotifyPeer.parse_inputNotifyForumTopic($0) }
dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) }
dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) }
+ dict[1009235855] = { return Api.InputPasskeyCredential.parse_inputPasskeyCredentialPublicKey($0) }
+ dict[-1021329078] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseLogin($0) }
+ dict[1046713180] = { return Api.InputPasskeyResponse.parse_inputPasskeyResponseRegister($0) }
dict[873977640] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentials($0) }
dict[178373535] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsApplePay($0) }
dict[-1966921727] = { return Api.InputPaymentCredentials.parse_inputPaymentCredentialsGooglePay($0) }
@@ -617,7 +620,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1189364422] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) }
dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) }
dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) }
- dict[-614898352] = { return Api.MessageAction.parse_messageActionStarGift($0) }
+ dict[-366202413] = { return Api.MessageAction.parse_messageActionStarGift($0) }
+ dict[2000845012] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOffer($0) }
+ dict[1940760427] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOfferDeclined($0) }
dict[-1787656893] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) }
dict[747579941] = { return Api.MessageAction.parse_messageActionSuggestBirthday($0) }
dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) }
@@ -756,6 +761,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[520887001] = { return Api.PaidReactionPrivacy.parse_paidReactionPrivacyAnonymous($0) }
dict[543872158] = { return Api.PaidReactionPrivacy.parse_paidReactionPrivacyDefault($0) }
dict[-596837136] = { return Api.PaidReactionPrivacy.parse_paidReactionPrivacyPeer($0) }
+ dict[-1738457409] = { return Api.Passkey.parse_passkey($0) }
dict[982592842] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow($0) }
dict[-732254058] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoUnknown($0) }
dict[-368917890] = { return Api.PaymentCharge.parse_paymentCharge($0) }
@@ -909,7 +915,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1681948327] = { return Api.SavedDialog.parse_monoForumDialog($0) }
dict[-1115174036] = { return Api.SavedDialog.parse_savedDialog($0) }
dict[-881854424] = { return Api.SavedReactionTag.parse_savedReactionTag($0) }
- dict[-1987861422] = { return Api.SavedStarGift.parse_savedStarGift($0) }
+ dict[-355041186] = { return Api.SavedStarGift.parse_savedStarGift($0) }
dict[1040931690] = { return Api.SearchPostsFlood.parse_searchPostsFlood($0) }
dict[-911191137] = { return Api.SearchResultsCalendarPeriod.parse_searchResultsCalendarPeriod($0) }
dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) }
@@ -974,8 +980,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) }
- dict[463097215] = { return Api.StarGift.parse_starGift($0) }
- dict[-1329630181] = { return Api.StarGift.parse_starGiftUnique($0) }
+ dict[825922887] = { return Api.StarGift.parse_starGift($0) }
+ dict[1453155529] = { return Api.StarGift.parse_starGiftUnique($0) }
dict[-753154979] = { return Api.StarGiftActiveAuctionState.parse_starGiftActiveAuctionState($0) }
dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) }
dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) }
@@ -985,11 +991,14 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[520210263] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdBackdrop($0) }
dict[1219145276] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdModel($0) }
dict[1242965043] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdPattern($0) }
- dict[-1419714037] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) }
- dict[1571835723] = { return Api.StarGiftAuctionState.parse_starGiftAuctionState($0) }
- dict[2107014202] = { return Api.StarGiftAuctionState.parse_starGiftAuctionStateFinished($0) }
+ dict[1118831432] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) }
+ dict[984483112] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRound($0) }
+ dict[178266597] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRoundExtendable($0) }
+ dict[1998212710] = { return Api.StarGiftAuctionState.parse_starGiftAuctionState($0) }
+ dict[-1758614593] = { return Api.StarGiftAuctionState.parse_starGiftAuctionStateFinished($0) }
dict[-30197422] = { return Api.StarGiftAuctionState.parse_starGiftAuctionStateNotModified($0) }
dict[787403204] = { return Api.StarGiftAuctionUserState.parse_starGiftAuctionUserState($0) }
+ dict[-1342872680] = { return Api.StarGiftBackground.parse_starGiftBackground($0) }
dict[-1653926992] = { return Api.StarGiftCollection.parse_starGiftCollection($0) }
dict[-1712704739] = { return Api.StarGiftUpgradePrice.parse_starGiftUpgradePrice($0) }
dict[-586389774] = { return Api.StarRefProgram.parse_starRefProgram($0) }
@@ -1246,7 +1255,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[555358088] = { return Api.WebPage.parse_webPageEmpty($0) }
dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) }
dict[-1328464313] = { return Api.WebPage.parse_webPagePending($0) }
- dict[55150251] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftAuction($0) }
+ dict[29770178] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftAuction($0) }
dict[835375875] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftCollection($0) }
dict[1355547603] = { return Api.WebPageAttribute.parse_webPageAttributeStickerSet($0) }
dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) }
@@ -1268,6 +1277,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1866176559] = { return Api.account.EmojiStatuses.parse_emojiStatuses($0) }
dict[-796072379] = { return Api.account.EmojiStatuses.parse_emojiStatusesNotModified($0) }
dict[504403720] = { return Api.account.PaidMessagesRevenue.parse_paidMessagesRevenue($0) }
+ dict[-513057567] = { return Api.account.PasskeyRegistrationOptions.parse_passkeyRegistrationOptions($0) }
+ dict[-119494116] = { return Api.account.Passkeys.parse_passkeys($0) }
dict[-1787080453] = { return Api.account.Password.parse_password($0) }
dict[-1036572727] = { return Api.account.PasswordInputSettings.parse_passwordInputSettings($0) }
dict[-1705233435] = { return Api.account.PasswordSettings.parse_passwordSettings($0) }
@@ -1302,6 +1313,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1654593920] = { return Api.auth.LoginToken.parse_loginToken($0) }
dict[110008598] = { return Api.auth.LoginToken.parse_loginTokenMigrateTo($0) }
dict[957176926] = { return Api.auth.LoginToken.parse_loginTokenSuccess($0) }
+ dict[-503089271] = { return Api.auth.PasskeyLoginOptions.parse_passkeyLoginOptions($0) }
dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) }
dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) }
dict[-527082948] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) }
@@ -1484,12 +1496,13 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1803939105] = { return Api.payments.ResaleStarGifts.parse_resaleStarGifts($0) }
dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) }
dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
- dict[-1745778728] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctions($0) }
+ dict[-1359565892] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctions($0) }
dict[-617358640] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctionsNotModified($0) }
dict[2103169520] = { return Api.payments.StarGiftAuctionAcquiredGifts.parse_starGiftAuctionAcquiredGifts($0) }
- dict[244900980] = { return Api.payments.StarGiftAuctionState.parse_starGiftAuctionState($0) }
+ dict[1798960364] = { return Api.payments.StarGiftAuctionState.parse_starGiftAuctionState($0) }
dict[-1977011469] = { return Api.payments.StarGiftCollections.parse_starGiftCollections($0) }
dict[-1598402793] = { return Api.payments.StarGiftCollections.parse_starGiftCollectionsNotModified($0) }
+ dict[1187439471] = { return Api.payments.StarGiftUpgradeAttributes.parse_starGiftUpgradeAttributes($0) }
dict[1038213101] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) }
dict[-2069218660] = { return Api.payments.StarGiftWithdrawalUrl.parse_starGiftWithdrawalUrl($0) }
dict[785918357] = { return Api.payments.StarGifts.parse_starGifts($0) }
@@ -1930,6 +1943,10 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.InputNotifyPeer:
_1.serialize(buffer, boxed)
+ case let _1 as Api.InputPasskeyCredential:
+ _1.serialize(buffer, boxed)
+ case let _1 as Api.InputPasskeyResponse:
+ _1.serialize(buffer, boxed)
case let _1 as Api.InputPaymentCredentials:
_1.serialize(buffer, boxed)
case let _1 as Api.InputPeer:
@@ -2066,6 +2083,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.PaidReactionPrivacy:
_1.serialize(buffer, boxed)
+ case let _1 as Api.Passkey:
+ _1.serialize(buffer, boxed)
case let _1 as Api.PasswordKdfAlgo:
_1.serialize(buffer, boxed)
case let _1 as Api.PaymentCharge:
@@ -2228,10 +2247,14 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAuctionAcquiredGift:
_1.serialize(buffer, boxed)
+ case let _1 as Api.StarGiftAuctionRound:
+ _1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAuctionState:
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftAuctionUserState:
_1.serialize(buffer, boxed)
+ case let _1 as Api.StarGiftBackground:
+ _1.serialize(buffer, boxed)
case let _1 as Api.StarGiftCollection:
_1.serialize(buffer, boxed)
case let _1 as Api.StarGiftUpgradePrice:
@@ -2376,6 +2399,10 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.account.PaidMessagesRevenue:
_1.serialize(buffer, boxed)
+ case let _1 as Api.account.PasskeyRegistrationOptions:
+ _1.serialize(buffer, boxed)
+ case let _1 as Api.account.Passkeys:
+ _1.serialize(buffer, boxed)
case let _1 as Api.account.Password:
_1.serialize(buffer, boxed)
case let _1 as Api.account.PasswordInputSettings:
@@ -2416,6 +2443,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.auth.LoginToken:
_1.serialize(buffer, boxed)
+ case let _1 as Api.auth.PasskeyLoginOptions:
+ _1.serialize(buffer, boxed)
case let _1 as Api.auth.PasswordRecovery:
_1.serialize(buffer, boxed)
case let _1 as Api.auth.SentCode:
@@ -2656,6 +2685,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.payments.StarGiftCollections:
_1.serialize(buffer, boxed)
+ case let _1 as Api.payments.StarGiftUpgradeAttributes:
+ _1.serialize(buffer, boxed)
case let _1 as Api.payments.StarGiftUpgradePreview:
_1.serialize(buffer, boxed)
case let _1 as Api.payments.StarGiftWithdrawalUrl:
diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift
index 55ba981c95..4914c64e63 100644
--- a/submodules/TelegramApi/Sources/Api11.swift
+++ b/submodules/TelegramApi/Sources/Api11.swift
@@ -868,6 +868,128 @@ public extension Api {
}
}
+public extension Api {
+ enum InputPasskeyCredential: TypeConstructorDescription {
+ case inputPasskeyCredentialPublicKey(id: String, rawId: String, response: Api.InputPasskeyResponse)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .inputPasskeyCredentialPublicKey(let id, let rawId, let response):
+ if boxed {
+ buffer.appendInt32(1009235855)
+ }
+ serializeString(id, buffer: buffer, boxed: false)
+ serializeString(rawId, buffer: buffer, boxed: false)
+ response.serialize(buffer, true)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .inputPasskeyCredentialPublicKey(let id, let rawId, let response):
+ return ("inputPasskeyCredentialPublicKey", [("id", id as Any), ("rawId", rawId as Any), ("response", response as Any)])
+ }
+ }
+
+ public static func parse_inputPasskeyCredentialPublicKey(_ reader: BufferReader) -> InputPasskeyCredential? {
+ var _1: String?
+ _1 = parseString(reader)
+ var _2: String?
+ _2 = parseString(reader)
+ var _3: Api.InputPasskeyResponse?
+ if let signature = reader.readInt32() {
+ _3 = Api.parse(reader, signature: signature) as? Api.InputPasskeyResponse
+ }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ if _c1 && _c2 && _c3 {
+ return Api.InputPasskeyCredential.inputPasskeyCredentialPublicKey(id: _1!, rawId: _2!, response: _3!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ enum InputPasskeyResponse: TypeConstructorDescription {
+ case inputPasskeyResponseLogin(clientData: Api.DataJSON, authenticatorData: Buffer, signature: Buffer, userHandle: String)
+ case inputPasskeyResponseRegister(clientData: Api.DataJSON, attestationData: Buffer)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .inputPasskeyResponseLogin(let clientData, let authenticatorData, let signature, let userHandle):
+ if boxed {
+ buffer.appendInt32(-1021329078)
+ }
+ clientData.serialize(buffer, true)
+ serializeBytes(authenticatorData, buffer: buffer, boxed: false)
+ serializeBytes(signature, buffer: buffer, boxed: false)
+ serializeString(userHandle, buffer: buffer, boxed: false)
+ break
+ case .inputPasskeyResponseRegister(let clientData, let attestationData):
+ if boxed {
+ buffer.appendInt32(1046713180)
+ }
+ clientData.serialize(buffer, true)
+ serializeBytes(attestationData, buffer: buffer, boxed: false)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .inputPasskeyResponseLogin(let clientData, let authenticatorData, let signature, let userHandle):
+ return ("inputPasskeyResponseLogin", [("clientData", clientData as Any), ("authenticatorData", authenticatorData as Any), ("signature", signature as Any), ("userHandle", userHandle as Any)])
+ case .inputPasskeyResponseRegister(let clientData, let attestationData):
+ return ("inputPasskeyResponseRegister", [("clientData", clientData as Any), ("attestationData", attestationData as Any)])
+ }
+ }
+
+ public static func parse_inputPasskeyResponseLogin(_ reader: BufferReader) -> InputPasskeyResponse? {
+ var _1: Api.DataJSON?
+ if let signature = reader.readInt32() {
+ _1 = Api.parse(reader, signature: signature) as? Api.DataJSON
+ }
+ var _2: Buffer?
+ _2 = parseBytes(reader)
+ var _3: Buffer?
+ _3 = parseBytes(reader)
+ var _4: String?
+ _4 = parseString(reader)
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ if _c1 && _c2 && _c3 && _c4 {
+ return Api.InputPasskeyResponse.inputPasskeyResponseLogin(clientData: _1!, authenticatorData: _2!, signature: _3!, userHandle: _4!)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_inputPasskeyResponseRegister(_ reader: BufferReader) -> InputPasskeyResponse? {
+ var _1: Api.DataJSON?
+ if let signature = reader.readInt32() {
+ _1 = Api.parse(reader, signature: signature) as? Api.DataJSON
+ }
+ var _2: Buffer?
+ _2 = parseBytes(reader)
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ if _c1 && _c2 {
+ return Api.InputPasskeyResponse.inputPasskeyResponseRegister(clientData: _1!, attestationData: _2!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api {
enum InputPaymentCredentials: TypeConstructorDescription {
case inputPaymentCredentials(flags: Int32, data: Api.DataJSON)
diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift
index 40303517e8..4bb0329f83 100644
--- a/submodules/TelegramApi/Sources/Api15.swift
+++ b/submodules/TelegramApi/Sources/Api15.swift
@@ -1069,7 +1069,9 @@ public extension Api {
case messageActionSetChatTheme(theme: Api.ChatTheme)
case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper)
case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?)
- case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?, giftMsgId: Int32?, toId: Api.Peer?)
+ case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?, giftMsgId: Int32?, toId: Api.Peer?, giftNum: Int32?)
+ case messageActionStarGiftPurchaseOffer(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount, expiresAt: Int32)
+ case messageActionStarGiftPurchaseOfferDeclined(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount)
case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleAmount: Api.StarsAmount?, canTransferAt: Int32?, canResellAt: Int32?, dropOriginalDetailsStars: Int64?)
case messageActionSuggestBirthday(birthday: Api.Birthday)
case messageActionSuggestProfilePhoto(photo: Api.Photo)
@@ -1460,9 +1462,9 @@ public extension Api {
serializeInt32(period, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)}
break
- case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId):
+ case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId, let giftNum):
if boxed {
- buffer.appendInt32(-614898352)
+ buffer.appendInt32(-366202413)
}
serializeInt32(flags, buffer: buffer, boxed: false)
gift.serialize(buffer, true)
@@ -1476,6 +1478,24 @@ public extension Api {
if Int(flags) & Int(1 << 14) != 0 {serializeString(prepaidUpgradeHash!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 15) != 0 {serializeInt32(giftMsgId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 18) != 0 {toId!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 19) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)}
+ break
+ case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt):
+ if boxed {
+ buffer.appendInt32(2000845012)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ gift.serialize(buffer, true)
+ price.serialize(buffer, true)
+ serializeInt32(expiresAt, buffer: buffer, boxed: false)
+ break
+ case .messageActionStarGiftPurchaseOfferDeclined(let flags, let gift, let price):
+ if boxed {
+ buffer.appendInt32(1940760427)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ gift.serialize(buffer, true)
+ price.serialize(buffer, true)
break
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt, let dropOriginalDetailsStars):
if boxed {
@@ -1678,8 +1698,12 @@ public extension Api {
return ("messageActionSetChatWallPaper", [("flags", flags as Any), ("wallpaper", wallpaper as Any)])
case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom):
return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)])
- case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId):
- return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("giftMsgId", giftMsgId as Any), ("toId", toId as Any)])
+ case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId, let giftNum):
+ return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("giftMsgId", giftMsgId as Any), ("toId", toId as Any), ("giftNum", giftNum as Any)])
+ case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt):
+ return ("messageActionStarGiftPurchaseOffer", [("flags", flags as Any), ("gift", gift as Any), ("price", price as Any), ("expiresAt", expiresAt as Any)])
+ case .messageActionStarGiftPurchaseOfferDeclined(let flags, let gift, let price):
+ return ("messageActionStarGiftPurchaseOfferDeclined", [("flags", flags as Any), ("gift", gift as Any), ("price", price as Any)])
case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt, let dropOriginalDetailsStars):
return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleAmount", resaleAmount as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any)])
case .messageActionSuggestBirthday(let birthday):
@@ -2454,6 +2478,8 @@ public extension Api {
if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() {
_12 = Api.parse(reader, signature: signature) as? Api.Peer
} }
+ var _13: Int32?
+ if Int(_1!) & Int(1 << 19) != 0 {_13 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
@@ -2466,8 +2492,54 @@ public extension Api {
let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil
let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil
let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
- return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10, giftMsgId: _11, toId: _12)
+ let _c13 = (Int(_1!) & Int(1 << 19) == 0) || _13 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
+ return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10, giftMsgId: _11, toId: _12, giftNum: _13)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_messageActionStarGiftPurchaseOffer(_ reader: BufferReader) -> MessageAction? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Api.StarGift?
+ if let signature = reader.readInt32() {
+ _2 = Api.parse(reader, signature: signature) as? Api.StarGift
+ }
+ var _3: Api.StarsAmount?
+ if let signature = reader.readInt32() {
+ _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount
+ }
+ var _4: Int32?
+ _4 = reader.readInt32()
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ if _c1 && _c2 && _c3 && _c4 {
+ return Api.MessageAction.messageActionStarGiftPurchaseOffer(flags: _1!, gift: _2!, price: _3!, expiresAt: _4!)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_messageActionStarGiftPurchaseOfferDeclined(_ reader: BufferReader) -> MessageAction? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Api.StarGift?
+ if let signature = reader.readInt32() {
+ _2 = Api.parse(reader, signature: signature) as? Api.StarGift
+ }
+ var _3: Api.StarsAmount?
+ if let signature = reader.readInt32() {
+ _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount
+ }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ if _c1 && _c2 && _c3 {
+ return Api.MessageAction.messageActionStarGiftPurchaseOfferDeclined(flags: _1!, gift: _2!, price: _3!)
}
else {
return nil
diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift
index 1512252096..2cb2a538fd 100644
--- a/submodules/TelegramApi/Sources/Api19.swift
+++ b/submodules/TelegramApi/Sources/Api19.swift
@@ -396,6 +396,62 @@ public extension Api {
}
}
+public extension Api {
+ enum Passkey: TypeConstructorDescription {
+ case passkey(flags: Int32, id: String, name: String, date: Int32, softwareEmojiId: Int64?, lastUsageDate: Int32?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .passkey(let flags, let id, let name, let date, let softwareEmojiId, let lastUsageDate):
+ if boxed {
+ buffer.appendInt32(-1738457409)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeString(id, buffer: buffer, boxed: false)
+ serializeString(name, buffer: buffer, boxed: false)
+ serializeInt32(date, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt64(softwareEmojiId!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastUsageDate!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .passkey(let flags, let id, let name, let date, let softwareEmojiId, let lastUsageDate):
+ return ("passkey", [("flags", flags as Any), ("id", id as Any), ("name", name as Any), ("date", date as Any), ("softwareEmojiId", softwareEmojiId as Any), ("lastUsageDate", lastUsageDate as Any)])
+ }
+ }
+
+ public static func parse_passkey(_ reader: BufferReader) -> Passkey? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: String?
+ _2 = parseString(reader)
+ var _3: String?
+ _3 = parseString(reader)
+ var _4: Int32?
+ _4 = reader.readInt32()
+ var _5: Int64?
+ if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt64() }
+ var _6: Int32?
+ if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
+ let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
+ return Api.Passkey.passkey(flags: _1!, id: _2!, name: _3!, date: _4!, softwareEmojiId: _5, lastUsageDate: _6)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api {
enum PasswordKdfAlgo: TypeConstructorDescription {
case passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow(salt1: Buffer, salt2: Buffer, g: Int32, p: Buffer)
diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift
index 1c602ab476..de67bb1fd4 100644
--- a/submodules/TelegramApi/Sources/Api23.swift
+++ b/submodules/TelegramApi/Sources/Api23.swift
@@ -626,13 +626,13 @@ public extension Api {
}
public extension Api {
enum SavedStarGift: TypeConstructorDescription {
- case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?, collectionId: [Int32]?, prepaidUpgradeHash: String?, dropOriginalDetailsStars: Int64?)
+ case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?, collectionId: [Int32]?, prepaidUpgradeHash: String?, dropOriginalDetailsStars: Int64?, giftNum: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars):
+ case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars, let giftNum):
if boxed {
- buffer.appendInt32(-1987861422)
+ buffer.appendInt32(-355041186)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {fromId!.serialize(buffer, true)}
@@ -654,14 +654,15 @@ public extension Api {
}}
if Int(flags) & Int(1 << 16) != 0 {serializeString(prepaidUpgradeHash!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 18) != 0 {serializeInt64(dropOriginalDetailsStars!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 19) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars):
- return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("collectionId", collectionId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any)])
+ case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars, let giftNum):
+ return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("collectionId", collectionId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any), ("giftNum", giftNum as Any)])
}
}
@@ -706,6 +707,8 @@ public extension Api {
if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) }
var _16: Int64?
if Int(_1!) & Int(1 << 18) != 0 {_16 = reader.readInt64() }
+ var _17: Int32?
+ if Int(_1!) & Int(1 << 19) != 0 {_17 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
let _c3 = _3 != nil
@@ -722,8 +725,9 @@ public extension Api {
let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 18) == 0) || _16 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 {
- return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14, prepaidUpgradeHash: _15, dropOriginalDetailsStars: _16)
+ let _c17 = (Int(_1!) & Int(1 << 19) == 0) || _17 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
+ return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14, prepaidUpgradeHash: _15, dropOriginalDetailsStars: _16, giftNum: _17)
}
else {
return nil
diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift
index 5e4db299ba..5abc7a1dcd 100644
--- a/submodules/TelegramApi/Sources/Api25.swift
+++ b/submodules/TelegramApi/Sources/Api25.swift
@@ -288,14 +288,14 @@ public extension Api {
}
public extension Api {
enum StarGift: TypeConstructorDescription {
- case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?, auctionSlug: String?, giftsPerRound: Int32?)
- case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?)
+ case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?, auctionSlug: String?, giftsPerRound: Int32?, auctionStartDate: Int32?, upgradeVariants: Int32?, background: Api.StarGiftBackground?)
+ case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, valueUsdAmount: Int64?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?, offerMinStars: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound):
+ case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound, let auctionStartDate, let upgradeVariants, let background):
if boxed {
- buffer.appendInt32(463097215)
+ buffer.appendInt32(825922887)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
@@ -316,10 +316,13 @@ public extension Api {
if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 11) != 0 {serializeString(auctionSlug!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 11) != 0 {serializeInt32(giftsPerRound!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 11) != 0 {serializeInt32(auctionStartDate!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 12) != 0 {serializeInt32(upgradeVariants!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 13) != 0 {background!.serialize(buffer, true)}
break
- case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId):
+ case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let valueUsdAmount, let themePeer, let peerColor, let hostId, let offerMinStars):
if boxed {
- buffer.appendInt32(-1329630181)
+ buffer.appendInt32(1453155529)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(id, buffer: buffer, boxed: false)
@@ -346,19 +349,21 @@ public extension Api {
if Int(flags) & Int(1 << 5) != 0 {releasedBy!.serialize(buffer, true)}
if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 8) != 0 {serializeString(valueCurrency!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueUsdAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 10) != 0 {themePeer!.serialize(buffer, true)}
if Int(flags) & Int(1 << 11) != 0 {peerColor!.serialize(buffer, true)}
if Int(flags) & Int(1 << 12) != 0 {hostId!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 13) != 0 {serializeInt32(offerMinStars!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound):
- return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any), ("auctionSlug", auctionSlug as Any), ("giftsPerRound", giftsPerRound as Any)])
- case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId):
- return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any)])
+ case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound, let auctionStartDate, let upgradeVariants, let background):
+ return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any), ("auctionSlug", auctionSlug as Any), ("giftsPerRound", giftsPerRound as Any), ("auctionStartDate", auctionStartDate as Any), ("upgradeVariants", upgradeVariants as Any), ("background", background as Any)])
+ case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let valueUsdAmount, let themePeer, let peerColor, let hostId, let offerMinStars):
+ return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("valueUsdAmount", valueUsdAmount as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any), ("offerMinStars", offerMinStars as Any)])
}
}
@@ -405,6 +410,14 @@ public extension Api {
if Int(_1!) & Int(1 << 11) != 0 {_18 = parseString(reader) }
var _19: Int32?
if Int(_1!) & Int(1 << 11) != 0 {_19 = reader.readInt32() }
+ var _20: Int32?
+ if Int(_1!) & Int(1 << 11) != 0 {_20 = reader.readInt32() }
+ var _21: Int32?
+ if Int(_1!) & Int(1 << 12) != 0 {_21 = reader.readInt32() }
+ var _22: Api.StarGiftBackground?
+ if Int(_1!) & Int(1 << 13) != 0 {if let signature = reader.readInt32() {
+ _22 = Api.parse(reader, signature: signature) as? Api.StarGiftBackground
+ } }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@@ -424,8 +437,11 @@ public extension Api {
let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil
let _c18 = (Int(_1!) & Int(1 << 11) == 0) || _18 != nil
let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 {
- return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17, auctionSlug: _18, giftsPerRound: _19)
+ let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil
+ let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil
+ let _c22 = (Int(_1!) & Int(1 << 13) == 0) || _22 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 {
+ return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17, auctionSlug: _18, giftsPerRound: _19, auctionStartDate: _20, upgradeVariants: _21, background: _22)
}
else {
return nil
@@ -474,18 +490,22 @@ public extension Api {
if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() }
var _17: String?
if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) }
- var _18: Api.Peer?
+ var _18: Int64?
+ if Int(_1!) & Int(1 << 8) != 0 {_18 = reader.readInt64() }
+ var _19: Api.Peer?
if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() {
- _18 = Api.parse(reader, signature: signature) as? Api.Peer
+ _19 = Api.parse(reader, signature: signature) as? Api.Peer
} }
- var _19: Api.PeerColor?
+ var _20: Api.PeerColor?
if Int(_1!) & Int(1 << 11) != 0 {if let signature = reader.readInt32() {
- _19 = Api.parse(reader, signature: signature) as? Api.PeerColor
+ _20 = Api.parse(reader, signature: signature) as? Api.PeerColor
} }
- var _20: Api.Peer?
+ var _21: Api.Peer?
if Int(_1!) & Int(1 << 12) != 0 {if let signature = reader.readInt32() {
- _20 = Api.parse(reader, signature: signature) as? Api.Peer
+ _21 = Api.parse(reader, signature: signature) as? Api.Peer
} }
+ var _22: Int32?
+ if Int(_1!) & Int(1 << 13) != 0 {_22 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@@ -503,11 +523,13 @@ public extension Api {
let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil
let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil
let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil
- let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil
- let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil
- let _c20 = (Int(_1!) & Int(1 << 12) == 0) || _20 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 {
- return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18, peerColor: _19, hostId: _20)
+ let _c18 = (Int(_1!) & Int(1 << 8) == 0) || _18 != nil
+ let _c19 = (Int(_1!) & Int(1 << 10) == 0) || _19 != nil
+ let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil
+ let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil
+ let _c22 = (Int(_1!) & Int(1 << 13) == 0) || _22 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 {
+ return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, valueUsdAmount: _18, themePeer: _19, peerColor: _20, hostId: _21, offerMinStars: _22)
}
else {
return nil
@@ -848,13 +870,13 @@ public extension Api {
}
public extension Api {
enum StarGiftAuctionAcquiredGift: TypeConstructorDescription {
- case starGiftAuctionAcquiredGift(flags: Int32, peer: Api.Peer, date: Int32, bidAmount: Int64, round: Int32, pos: Int32, message: Api.TextWithEntities?)
+ case starGiftAuctionAcquiredGift(flags: Int32, peer: Api.Peer, date: Int32, bidAmount: Int64, round: Int32, pos: Int32, message: Api.TextWithEntities?, giftNum: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message):
+ case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message, let giftNum):
if boxed {
- buffer.appendInt32(-1419714037)
+ buffer.appendInt32(1118831432)
}
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
@@ -863,14 +885,15 @@ public extension Api {
serializeInt32(round, buffer: buffer, boxed: false)
serializeInt32(pos, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 2) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message):
- return ("starGiftAuctionAcquiredGift", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("bidAmount", bidAmount as Any), ("round", round as Any), ("pos", pos as Any), ("message", message as Any)])
+ case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message, let giftNum):
+ return ("starGiftAuctionAcquiredGift", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("bidAmount", bidAmount as Any), ("round", round as Any), ("pos", pos as Any), ("message", message as Any), ("giftNum", giftNum as Any)])
}
}
@@ -893,6 +916,8 @@ public extension Api {
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
+ var _8: Int32?
+ if Int(_1!) & Int(1 << 2) != 0 {_8 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@@ -900,8 +925,81 @@ public extension Api {
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
- return Api.StarGiftAuctionAcquiredGift.starGiftAuctionAcquiredGift(flags: _1!, peer: _2!, date: _3!, bidAmount: _4!, round: _5!, pos: _6!, message: _7)
+ let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
+ return Api.StarGiftAuctionAcquiredGift.starGiftAuctionAcquiredGift(flags: _1!, peer: _2!, date: _3!, bidAmount: _4!, round: _5!, pos: _6!, message: _7, giftNum: _8)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ enum StarGiftAuctionRound: TypeConstructorDescription {
+ case starGiftAuctionRound(num: Int32, duration: Int32)
+ case starGiftAuctionRoundExtendable(num: Int32, duration: Int32, extendTop: Int32, extendWindow: Int32)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starGiftAuctionRound(let num, let duration):
+ if boxed {
+ buffer.appendInt32(984483112)
+ }
+ serializeInt32(num, buffer: buffer, boxed: false)
+ serializeInt32(duration, buffer: buffer, boxed: false)
+ break
+ case .starGiftAuctionRoundExtendable(let num, let duration, let extendTop, let extendWindow):
+ if boxed {
+ buffer.appendInt32(178266597)
+ }
+ serializeInt32(num, buffer: buffer, boxed: false)
+ serializeInt32(duration, buffer: buffer, boxed: false)
+ serializeInt32(extendTop, buffer: buffer, boxed: false)
+ serializeInt32(extendWindow, buffer: buffer, boxed: false)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starGiftAuctionRound(let num, let duration):
+ return ("starGiftAuctionRound", [("num", num as Any), ("duration", duration as Any)])
+ case .starGiftAuctionRoundExtendable(let num, let duration, let extendTop, let extendWindow):
+ return ("starGiftAuctionRoundExtendable", [("num", num as Any), ("duration", duration as Any), ("extendTop", extendTop as Any), ("extendWindow", extendWindow as Any)])
+ }
+ }
+
+ public static func parse_starGiftAuctionRound(_ reader: BufferReader) -> StarGiftAuctionRound? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ if _c1 && _c2 {
+ return Api.StarGiftAuctionRound.starGiftAuctionRound(num: _1!, duration: _2!)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_starGiftAuctionRoundExtendable(_ reader: BufferReader) -> StarGiftAuctionRound? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: Int32?
+ _3 = reader.readInt32()
+ var _4: Int32?
+ _4 = reader.readInt32()
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ if _c1 && _c2 && _c3 && _c4 {
+ return Api.StarGiftAuctionRound.starGiftAuctionRoundExtendable(num: _1!, duration: _2!, extendTop: _3!, extendWindow: _4!)
}
else {
return nil
@@ -912,15 +1010,15 @@ public extension Api {
}
public extension Api {
enum StarGiftAuctionState: TypeConstructorDescription {
- case starGiftAuctionState(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [Api.AuctionBidLevel], topBidders: [Int64], nextRoundAt: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32)
- case starGiftAuctionStateFinished(startDate: Int32, endDate: Int32, averagePrice: Int64)
+ case starGiftAuctionState(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [Api.AuctionBidLevel], topBidders: [Int64], nextRoundAt: Int32, lastGiftNum: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32, rounds: [Api.StarGiftAuctionRound])
+ case starGiftAuctionStateFinished(flags: Int32, startDate: Int32, endDate: Int32, averagePrice: Int64, listedCount: Int32?, fragmentListedCount: Int32?, fragmentListedUrl: String?)
case starGiftAuctionStateNotModified
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let giftsLeft, let currentRound, let totalRounds):
+ case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let lastGiftNum, let giftsLeft, let currentRound, let totalRounds, let rounds):
if boxed {
- buffer.appendInt32(1571835723)
+ buffer.appendInt32(1998212710)
}
serializeInt32(version, buffer: buffer, boxed: false)
serializeInt32(startDate, buffer: buffer, boxed: false)
@@ -937,17 +1035,27 @@ public extension Api {
serializeInt64(item, buffer: buffer, boxed: false)
}
serializeInt32(nextRoundAt, buffer: buffer, boxed: false)
+ serializeInt32(lastGiftNum, buffer: buffer, boxed: false)
serializeInt32(giftsLeft, buffer: buffer, boxed: false)
serializeInt32(currentRound, buffer: buffer, boxed: false)
serializeInt32(totalRounds, buffer: buffer, boxed: false)
- break
- case .starGiftAuctionStateFinished(let startDate, let endDate, let averagePrice):
- if boxed {
- buffer.appendInt32(2107014202)
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(rounds.count))
+ for item in rounds {
+ item.serialize(buffer, true)
}
+ break
+ case .starGiftAuctionStateFinished(let flags, let startDate, let endDate, let averagePrice, let listedCount, let fragmentListedCount, let fragmentListedUrl):
+ if boxed {
+ buffer.appendInt32(-1758614593)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(startDate, buffer: buffer, boxed: false)
serializeInt32(endDate, buffer: buffer, boxed: false)
serializeInt64(averagePrice, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt32(listedCount!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 1) != 0 {serializeInt32(fragmentListedCount!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 1) != 0 {serializeString(fragmentListedUrl!, buffer: buffer, boxed: false)}
break
case .starGiftAuctionStateNotModified:
if boxed {
@@ -960,10 +1068,10 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let giftsLeft, let currentRound, let totalRounds):
- return ("starGiftAuctionState", [("version", version as Any), ("startDate", startDate as Any), ("endDate", endDate as Any), ("minBidAmount", minBidAmount as Any), ("bidLevels", bidLevels as Any), ("topBidders", topBidders as Any), ("nextRoundAt", nextRoundAt as Any), ("giftsLeft", giftsLeft as Any), ("currentRound", currentRound as Any), ("totalRounds", totalRounds as Any)])
- case .starGiftAuctionStateFinished(let startDate, let endDate, let averagePrice):
- return ("starGiftAuctionStateFinished", [("startDate", startDate as Any), ("endDate", endDate as Any), ("averagePrice", averagePrice as Any)])
+ case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let lastGiftNum, let giftsLeft, let currentRound, let totalRounds, let rounds):
+ return ("starGiftAuctionState", [("version", version as Any), ("startDate", startDate as Any), ("endDate", endDate as Any), ("minBidAmount", minBidAmount as Any), ("bidLevels", bidLevels as Any), ("topBidders", topBidders as Any), ("nextRoundAt", nextRoundAt as Any), ("lastGiftNum", lastGiftNum as Any), ("giftsLeft", giftsLeft as Any), ("currentRound", currentRound as Any), ("totalRounds", totalRounds as Any), ("rounds", rounds as Any)])
+ case .starGiftAuctionStateFinished(let flags, let startDate, let endDate, let averagePrice, let listedCount, let fragmentListedCount, let fragmentListedUrl):
+ return ("starGiftAuctionStateFinished", [("flags", flags as Any), ("startDate", startDate as Any), ("endDate", endDate as Any), ("averagePrice", averagePrice as Any), ("listedCount", listedCount as Any), ("fragmentListedCount", fragmentListedCount as Any), ("fragmentListedUrl", fragmentListedUrl as Any)])
case .starGiftAuctionStateNotModified:
return ("starGiftAuctionStateNotModified", [])
}
@@ -994,6 +1102,12 @@ public extension Api {
_9 = reader.readInt32()
var _10: Int32?
_10 = reader.readInt32()
+ var _11: Int32?
+ _11 = reader.readInt32()
+ var _12: [Api.StarGiftAuctionRound]?
+ if let _ = reader.readInt32() {
+ _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAuctionRound.self)
+ }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@@ -1004,8 +1118,10 @@ public extension Api {
let _c8 = _8 != nil
let _c9 = _9 != nil
let _c10 = _10 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
- return Api.StarGiftAuctionState.starGiftAuctionState(version: _1!, startDate: _2!, endDate: _3!, minBidAmount: _4!, bidLevels: _5!, topBidders: _6!, nextRoundAt: _7!, giftsLeft: _8!, currentRound: _9!, totalRounds: _10!)
+ let _c11 = _11 != nil
+ let _c12 = _12 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 {
+ return Api.StarGiftAuctionState.starGiftAuctionState(version: _1!, startDate: _2!, endDate: _3!, minBidAmount: _4!, bidLevels: _5!, topBidders: _6!, nextRoundAt: _7!, lastGiftNum: _8!, giftsLeft: _9!, currentRound: _10!, totalRounds: _11!, rounds: _12!)
}
else {
return nil
@@ -1016,13 +1132,25 @@ public extension Api {
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
- var _3: Int64?
- _3 = reader.readInt64()
+ var _3: Int32?
+ _3 = reader.readInt32()
+ var _4: Int64?
+ _4 = reader.readInt64()
+ var _5: Int32?
+ if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() }
+ var _6: Int32?
+ if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
+ var _7: String?
+ if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
- if _c1 && _c2 && _c3 {
- return Api.StarGiftAuctionState.starGiftAuctionStateFinished(startDate: _1!, endDate: _2!, averagePrice: _3!)
+ let _c4 = _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
+ let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
+ let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
+ return Api.StarGiftAuctionState.starGiftAuctionStateFinished(flags: _1!, startDate: _2!, endDate: _3!, averagePrice: _4!, listedCount: _5, fragmentListedCount: _6, fragmentListedUrl: _7)
}
else {
return nil
@@ -1092,6 +1220,50 @@ public extension Api {
}
}
+public extension Api {
+ enum StarGiftBackground: TypeConstructorDescription {
+ case starGiftBackground(centerColor: Int32, edgeColor: Int32, textColor: Int32)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starGiftBackground(let centerColor, let edgeColor, let textColor):
+ if boxed {
+ buffer.appendInt32(-1342872680)
+ }
+ serializeInt32(centerColor, buffer: buffer, boxed: false)
+ serializeInt32(edgeColor, buffer: buffer, boxed: false)
+ serializeInt32(textColor, buffer: buffer, boxed: false)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starGiftBackground(let centerColor, let edgeColor, let textColor):
+ return ("starGiftBackground", [("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("textColor", textColor as Any)])
+ }
+ }
+
+ public static func parse_starGiftBackground(_ reader: BufferReader) -> StarGiftBackground? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: Int32?
+ _3 = reader.readInt32()
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ if _c1 && _c2 && _c3 {
+ return Api.StarGiftBackground.starGiftBackground(centerColor: _1!, edgeColor: _2!, textColor: _3!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api {
enum StarGiftCollection: TypeConstructorDescription {
case starGiftCollection(flags: Int32, collectionId: Int32, title: String, icon: Api.Document?, giftsCount: Int32, hash: Int64)
@@ -1470,187 +1642,3 @@ public extension Api {
}
}
-public extension Api {
- enum StarsRating: TypeConstructorDescription {
- case starsRating(flags: Int32, level: Int32, currentLevelStars: Int64, stars: Int64, nextLevelStars: Int64?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .starsRating(let flags, let level, let currentLevelStars, let stars, let nextLevelStars):
- if boxed {
- buffer.appendInt32(453922567)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeInt32(level, buffer: buffer, boxed: false)
- serializeInt64(currentLevelStars, buffer: buffer, boxed: false)
- serializeInt64(stars, buffer: buffer, boxed: false)
- if Int(flags) & Int(1 << 0) != 0 {serializeInt64(nextLevelStars!, buffer: buffer, boxed: false)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .starsRating(let flags, let level, let currentLevelStars, let stars, let nextLevelStars):
- return ("starsRating", [("flags", flags as Any), ("level", level as Any), ("currentLevelStars", currentLevelStars as Any), ("stars", stars as Any), ("nextLevelStars", nextLevelStars as Any)])
- }
- }
-
- public static func parse_starsRating(_ reader: BufferReader) -> StarsRating? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Int32?
- _2 = reader.readInt32()
- var _3: Int64?
- _3 = reader.readInt64()
- var _4: Int64?
- _4 = reader.readInt64()
- var _5: Int64?
- if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt64() }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 {
- return Api.StarsRating.starsRating(flags: _1!, level: _2!, currentLevelStars: _3!, stars: _4!, nextLevelStars: _5)
- }
- else {
- return nil
- }
- }
-
- }
-}
-public extension Api {
- enum StarsRevenueStatus: TypeConstructorDescription {
- case starsRevenueStatus(flags: Int32, currentBalance: Api.StarsAmount, availableBalance: Api.StarsAmount, overallRevenue: Api.StarsAmount, nextWithdrawalAt: Int32?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
- if boxed {
- buffer.appendInt32(-21080943)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- currentBalance.serialize(buffer, true)
- availableBalance.serialize(buffer, true)
- overallRevenue.serialize(buffer, true)
- if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
- return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)])
- }
- }
-
- public static func parse_starsRevenueStatus(_ reader: BufferReader) -> StarsRevenueStatus? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Api.StarsAmount?
- if let signature = reader.readInt32() {
- _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount
- }
- var _3: Api.StarsAmount?
- if let signature = reader.readInt32() {
- _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount
- }
- var _4: Api.StarsAmount?
- if let signature = reader.readInt32() {
- _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount
- }
- var _5: Int32?
- if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 {
- return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5)
- }
- else {
- return nil
- }
- }
-
- }
-}
-public extension Api {
- enum StarsSubscription: TypeConstructorDescription {
- case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?, title: String?, photo: Api.WebDocument?, invoiceSlug: String?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug):
- if boxed {
- buffer.appendInt32(779004698)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeString(id, buffer: buffer, boxed: false)
- peer.serialize(buffer, true)
- serializeInt32(untilDate, buffer: buffer, boxed: false)
- pricing.serialize(buffer, true)
- if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)}
- if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
- if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 6) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug):
- return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any), ("title", title as Any), ("photo", photo as Any), ("invoiceSlug", invoiceSlug as Any)])
- }
- }
-
- public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: String?
- _2 = parseString(reader)
- var _3: Api.Peer?
- if let signature = reader.readInt32() {
- _3 = Api.parse(reader, signature: signature) as? Api.Peer
- }
- var _4: Int32?
- _4 = reader.readInt32()
- var _5: Api.StarsSubscriptionPricing?
- if let signature = reader.readInt32() {
- _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing
- }
- var _6: String?
- if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) }
- var _7: String?
- if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
- var _8: Api.WebDocument?
- if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
- _8 = Api.parse(reader, signature: signature) as? Api.WebDocument
- } }
- var _9: String?
- if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- let _c5 = _5 != nil
- let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
- let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
- let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil
- let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
- return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9)
- }
- else {
- return nil
- }
- }
-
- }
-}
diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift
index c6db21d4e0..fd08d27e51 100644
--- a/submodules/TelegramApi/Sources/Api26.swift
+++ b/submodules/TelegramApi/Sources/Api26.swift
@@ -1,3 +1,187 @@
+public extension Api {
+ enum StarsRating: TypeConstructorDescription {
+ case starsRating(flags: Int32, level: Int32, currentLevelStars: Int64, stars: Int64, nextLevelStars: Int64?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starsRating(let flags, let level, let currentLevelStars, let stars, let nextLevelStars):
+ if boxed {
+ buffer.appendInt32(453922567)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(level, buffer: buffer, boxed: false)
+ serializeInt64(currentLevelStars, buffer: buffer, boxed: false)
+ serializeInt64(stars, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt64(nextLevelStars!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starsRating(let flags, let level, let currentLevelStars, let stars, let nextLevelStars):
+ return ("starsRating", [("flags", flags as Any), ("level", level as Any), ("currentLevelStars", currentLevelStars as Any), ("stars", stars as Any), ("nextLevelStars", nextLevelStars as Any)])
+ }
+ }
+
+ public static func parse_starsRating(_ reader: BufferReader) -> StarsRating? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: Int64?
+ _3 = reader.readInt64()
+ var _4: Int64?
+ _4 = reader.readInt64()
+ var _5: Int64?
+ if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt64() }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 {
+ return Api.StarsRating.starsRating(flags: _1!, level: _2!, currentLevelStars: _3!, stars: _4!, nextLevelStars: _5)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ enum StarsRevenueStatus: TypeConstructorDescription {
+ case starsRevenueStatus(flags: Int32, currentBalance: Api.StarsAmount, availableBalance: Api.StarsAmount, overallRevenue: Api.StarsAmount, nextWithdrawalAt: Int32?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
+ if boxed {
+ buffer.appendInt32(-21080943)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ currentBalance.serialize(buffer, true)
+ availableBalance.serialize(buffer, true)
+ overallRevenue.serialize(buffer, true)
+ if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
+ return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)])
+ }
+ }
+
+ public static func parse_starsRevenueStatus(_ reader: BufferReader) -> StarsRevenueStatus? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Api.StarsAmount?
+ if let signature = reader.readInt32() {
+ _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount
+ }
+ var _3: Api.StarsAmount?
+ if let signature = reader.readInt32() {
+ _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount
+ }
+ var _4: Api.StarsAmount?
+ if let signature = reader.readInt32() {
+ _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount
+ }
+ var _5: Int32?
+ if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 {
+ return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ enum StarsSubscription: TypeConstructorDescription {
+ case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?, title: String?, photo: Api.WebDocument?, invoiceSlug: String?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug):
+ if boxed {
+ buffer.appendInt32(779004698)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeString(id, buffer: buffer, boxed: false)
+ peer.serialize(buffer, true)
+ serializeInt32(untilDate, buffer: buffer, boxed: false)
+ pricing.serialize(buffer, true)
+ if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 6) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug):
+ return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any), ("title", title as Any), ("photo", photo as Any), ("invoiceSlug", invoiceSlug as Any)])
+ }
+ }
+
+ public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: String?
+ _2 = parseString(reader)
+ var _3: Api.Peer?
+ if let signature = reader.readInt32() {
+ _3 = Api.parse(reader, signature: signature) as? Api.Peer
+ }
+ var _4: Int32?
+ _4 = reader.readInt32()
+ var _5: Api.StarsSubscriptionPricing?
+ if let signature = reader.readInt32() {
+ _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing
+ }
+ var _6: String?
+ if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) }
+ var _7: String?
+ if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) }
+ var _8: Api.WebDocument?
+ if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() {
+ _8 = Api.parse(reader, signature: signature) as? Api.WebDocument
+ } }
+ var _9: String?
+ if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ let _c5 = _5 != nil
+ let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil
+ let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil
+ let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil
+ let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
+ return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api {
enum StarsSubscriptionPricing: TypeConstructorDescription {
case starsSubscriptionPricing(period: Int32, amount: Int64)
@@ -1104,283 +1288,3 @@ public extension Api {
}
}
-public extension Api {
- enum StoryAlbum: TypeConstructorDescription {
- case storyAlbum(flags: Int32, albumId: Int32, title: String, iconPhoto: Api.Photo?, iconVideo: Api.Document?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo):
- if boxed {
- buffer.appendInt32(-1826262950)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeInt32(albumId, buffer: buffer, boxed: false)
- serializeString(title, buffer: buffer, boxed: false)
- if Int(flags) & Int(1 << 0) != 0 {iconPhoto!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 1) != 0 {iconVideo!.serialize(buffer, true)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo):
- return ("storyAlbum", [("flags", flags as Any), ("albumId", albumId as Any), ("title", title as Any), ("iconPhoto", iconPhoto as Any), ("iconVideo", iconVideo as Any)])
- }
- }
-
- public static func parse_storyAlbum(_ reader: BufferReader) -> StoryAlbum? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Int32?
- _2 = reader.readInt32()
- var _3: String?
- _3 = parseString(reader)
- var _4: Api.Photo?
- if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
- _4 = Api.parse(reader, signature: signature) as? Api.Photo
- } }
- var _5: Api.Document?
- if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
- _5 = Api.parse(reader, signature: signature) as? Api.Document
- } }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
- let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 {
- return Api.StoryAlbum.storyAlbum(flags: _1!, albumId: _2!, title: _3!, iconPhoto: _4, iconVideo: _5)
- }
- else {
- return nil
- }
- }
-
- }
-}
-public extension Api {
- enum StoryFwdHeader: TypeConstructorDescription {
- case storyFwdHeader(flags: Int32, from: Api.Peer?, fromName: String?, storyId: Int32?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .storyFwdHeader(let flags, let from, let fromName, let storyId):
- if boxed {
- buffer.appendInt32(-1205411504)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- if Int(flags) & Int(1 << 0) != 0 {from!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 1) != 0 {serializeString(fromName!, buffer: buffer, boxed: false)}
- if Int(flags) & Int(1 << 2) != 0 {serializeInt32(storyId!, buffer: buffer, boxed: false)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .storyFwdHeader(let flags, let from, let fromName, let storyId):
- return ("storyFwdHeader", [("flags", flags as Any), ("from", from as Any), ("fromName", fromName as Any), ("storyId", storyId as Any)])
- }
- }
-
- public static func parse_storyFwdHeader(_ reader: BufferReader) -> StoryFwdHeader? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Api.Peer?
- if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
- _2 = Api.parse(reader, signature: signature) as? Api.Peer
- } }
- var _3: String?
- if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) }
- var _4: Int32?
- if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() }
- let _c1 = _1 != nil
- let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
- let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
- let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
- if _c1 && _c2 && _c3 && _c4 {
- return Api.StoryFwdHeader.storyFwdHeader(flags: _1!, from: _2, fromName: _3, storyId: _4)
- }
- else {
- return nil
- }
- }
-
- }
-}
-public extension Api {
- indirect enum StoryItem: TypeConstructorDescription {
- case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?, albums: [Int32]?)
- case storyItemDeleted(id: Int32)
- case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums):
- if boxed {
- buffer.appendInt32(-302947087)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeInt32(id, buffer: buffer, boxed: false)
- serializeInt32(date, buffer: buffer, boxed: false)
- if Int(flags) & Int(1 << 18) != 0 {fromId!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 17) != 0 {fwdFrom!.serialize(buffer, true)}
- serializeInt32(expireDate, buffer: buffer, boxed: false)
- if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)}
- if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(entities!.count))
- for item in entities! {
- item.serialize(buffer, true)
- }}
- media.serialize(buffer, true)
- if Int(flags) & Int(1 << 14) != 0 {buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(mediaAreas!.count))
- for item in mediaAreas! {
- item.serialize(buffer, true)
- }}
- if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(privacy!.count))
- for item in privacy! {
- item.serialize(buffer, true)
- }}
- if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)}
- if Int(flags) & Int(1 << 19) != 0 {buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(albums!.count))
- for item in albums! {
- serializeInt32(item, buffer: buffer, boxed: false)
- }}
- break
- case .storyItemDeleted(let id):
- if boxed {
- buffer.appendInt32(1374088783)
- }
- serializeInt32(id, buffer: buffer, boxed: false)
- break
- case .storyItemSkipped(let flags, let id, let date, let expireDate):
- if boxed {
- buffer.appendInt32(-5388013)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeInt32(id, buffer: buffer, boxed: false)
- serializeInt32(date, buffer: buffer, boxed: false)
- serializeInt32(expireDate, buffer: buffer, boxed: false)
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums):
- return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any), ("albums", albums as Any)])
- case .storyItemDeleted(let id):
- return ("storyItemDeleted", [("id", id as Any)])
- case .storyItemSkipped(let flags, let id, let date, let expireDate):
- return ("storyItemSkipped", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any)])
- }
- }
-
- public static func parse_storyItem(_ reader: BufferReader) -> StoryItem? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Int32?
- _2 = reader.readInt32()
- var _3: Int32?
- _3 = reader.readInt32()
- var _4: Api.Peer?
- if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() {
- _4 = Api.parse(reader, signature: signature) as? Api.Peer
- } }
- var _5: Api.StoryFwdHeader?
- if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() {
- _5 = Api.parse(reader, signature: signature) as? Api.StoryFwdHeader
- } }
- var _6: Int32?
- _6 = reader.readInt32()
- var _7: String?
- if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) }
- var _8: [Api.MessageEntity]?
- if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
- _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
- } }
- var _9: Api.MessageMedia?
- if let signature = reader.readInt32() {
- _9 = Api.parse(reader, signature: signature) as? Api.MessageMedia
- }
- var _10: [Api.MediaArea]?
- if Int(_1!) & Int(1 << 14) != 0 {if let _ = reader.readInt32() {
- _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MediaArea.self)
- } }
- var _11: [Api.PrivacyRule]?
- if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
- _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self)
- } }
- var _12: Api.StoryViews?
- if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
- _12 = Api.parse(reader, signature: signature) as? Api.StoryViews
- } }
- var _13: Api.Reaction?
- if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() {
- _13 = Api.parse(reader, signature: signature) as? Api.Reaction
- } }
- var _14: [Int32]?
- if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() {
- _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
- } }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = (Int(_1!) & Int(1 << 18) == 0) || _4 != nil
- let _c5 = (Int(_1!) & Int(1 << 17) == 0) || _5 != nil
- let _c6 = _6 != nil
- let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil
- let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil
- let _c9 = _9 != nil
- let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil
- let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil
- let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil
- let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil
- let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
- return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14)
- }
- else {
- return nil
- }
- }
- public static func parse_storyItemDeleted(_ reader: BufferReader) -> StoryItem? {
- var _1: Int32?
- _1 = reader.readInt32()
- let _c1 = _1 != nil
- if _c1 {
- return Api.StoryItem.storyItemDeleted(id: _1!)
- }
- else {
- return nil
- }
- }
- public static func parse_storyItemSkipped(_ reader: BufferReader) -> StoryItem? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Int32?
- _2 = reader.readInt32()
- var _3: Int32?
- _3 = reader.readInt32()
- var _4: Int32?
- _4 = reader.readInt32()
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- if _c1 && _c2 && _c3 && _c4 {
- return Api.StoryItem.storyItemSkipped(flags: _1!, id: _2!, date: _3!, expireDate: _4!)
- }
- else {
- return nil
- }
- }
-
- }
-}
diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift
index 9327244759..075ddc821d 100644
--- a/submodules/TelegramApi/Sources/Api27.swift
+++ b/submodules/TelegramApi/Sources/Api27.swift
@@ -1,3 +1,283 @@
+public extension Api {
+ enum StoryAlbum: TypeConstructorDescription {
+ case storyAlbum(flags: Int32, albumId: Int32, title: String, iconPhoto: Api.Photo?, iconVideo: Api.Document?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo):
+ if boxed {
+ buffer.appendInt32(-1826262950)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(albumId, buffer: buffer, boxed: false)
+ serializeString(title, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {iconPhoto!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 1) != 0 {iconVideo!.serialize(buffer, true)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .storyAlbum(let flags, let albumId, let title, let iconPhoto, let iconVideo):
+ return ("storyAlbum", [("flags", flags as Any), ("albumId", albumId as Any), ("title", title as Any), ("iconPhoto", iconPhoto as Any), ("iconVideo", iconVideo as Any)])
+ }
+ }
+
+ public static func parse_storyAlbum(_ reader: BufferReader) -> StoryAlbum? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: String?
+ _3 = parseString(reader)
+ var _4: Api.Photo?
+ if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
+ _4 = Api.parse(reader, signature: signature) as? Api.Photo
+ } }
+ var _5: Api.Document?
+ if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
+ _5 = Api.parse(reader, signature: signature) as? Api.Document
+ } }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 {
+ return Api.StoryAlbum.storyAlbum(flags: _1!, albumId: _2!, title: _3!, iconPhoto: _4, iconVideo: _5)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ enum StoryFwdHeader: TypeConstructorDescription {
+ case storyFwdHeader(flags: Int32, from: Api.Peer?, fromName: String?, storyId: Int32?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .storyFwdHeader(let flags, let from, let fromName, let storyId):
+ if boxed {
+ buffer.appendInt32(-1205411504)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {from!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 1) != 0 {serializeString(fromName!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 2) != 0 {serializeInt32(storyId!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .storyFwdHeader(let flags, let from, let fromName, let storyId):
+ return ("storyFwdHeader", [("flags", flags as Any), ("from", from as Any), ("fromName", fromName as Any), ("storyId", storyId as Any)])
+ }
+ }
+
+ public static func parse_storyFwdHeader(_ reader: BufferReader) -> StoryFwdHeader? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Api.Peer?
+ if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
+ _2 = Api.parse(reader, signature: signature) as? Api.Peer
+ } }
+ var _3: String?
+ if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) }
+ var _4: Int32?
+ if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() }
+ let _c1 = _1 != nil
+ let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
+ let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
+ let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
+ if _c1 && _c2 && _c3 && _c4 {
+ return Api.StoryFwdHeader.storyFwdHeader(flags: _1!, from: _2, fromName: _3, storyId: _4)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api {
+ indirect enum StoryItem: TypeConstructorDescription {
+ case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?, albums: [Int32]?)
+ case storyItemDeleted(id: Int32)
+ case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums):
+ if boxed {
+ buffer.appendInt32(-302947087)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(id, buffer: buffer, boxed: false)
+ serializeInt32(date, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 18) != 0 {fromId!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 17) != 0 {fwdFrom!.serialize(buffer, true)}
+ serializeInt32(expireDate, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(entities!.count))
+ for item in entities! {
+ item.serialize(buffer, true)
+ }}
+ media.serialize(buffer, true)
+ if Int(flags) & Int(1 << 14) != 0 {buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(mediaAreas!.count))
+ for item in mediaAreas! {
+ item.serialize(buffer, true)
+ }}
+ if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(privacy!.count))
+ for item in privacy! {
+ item.serialize(buffer, true)
+ }}
+ if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 19) != 0 {buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(albums!.count))
+ for item in albums! {
+ serializeInt32(item, buffer: buffer, boxed: false)
+ }}
+ break
+ case .storyItemDeleted(let id):
+ if boxed {
+ buffer.appendInt32(1374088783)
+ }
+ serializeInt32(id, buffer: buffer, boxed: false)
+ break
+ case .storyItemSkipped(let flags, let id, let date, let expireDate):
+ if boxed {
+ buffer.appendInt32(-5388013)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(id, buffer: buffer, boxed: false)
+ serializeInt32(date, buffer: buffer, boxed: false)
+ serializeInt32(expireDate, buffer: buffer, boxed: false)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums):
+ return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any), ("albums", albums as Any)])
+ case .storyItemDeleted(let id):
+ return ("storyItemDeleted", [("id", id as Any)])
+ case .storyItemSkipped(let flags, let id, let date, let expireDate):
+ return ("storyItemSkipped", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any)])
+ }
+ }
+
+ public static func parse_storyItem(_ reader: BufferReader) -> StoryItem? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: Int32?
+ _3 = reader.readInt32()
+ var _4: Api.Peer?
+ if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() {
+ _4 = Api.parse(reader, signature: signature) as? Api.Peer
+ } }
+ var _5: Api.StoryFwdHeader?
+ if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() {
+ _5 = Api.parse(reader, signature: signature) as? Api.StoryFwdHeader
+ } }
+ var _6: Int32?
+ _6 = reader.readInt32()
+ var _7: String?
+ if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) }
+ var _8: [Api.MessageEntity]?
+ if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
+ _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self)
+ } }
+ var _9: Api.MessageMedia?
+ if let signature = reader.readInt32() {
+ _9 = Api.parse(reader, signature: signature) as? Api.MessageMedia
+ }
+ var _10: [Api.MediaArea]?
+ if Int(_1!) & Int(1 << 14) != 0 {if let _ = reader.readInt32() {
+ _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MediaArea.self)
+ } }
+ var _11: [Api.PrivacyRule]?
+ if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
+ _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self)
+ } }
+ var _12: Api.StoryViews?
+ if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() {
+ _12 = Api.parse(reader, signature: signature) as? Api.StoryViews
+ } }
+ var _13: Api.Reaction?
+ if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() {
+ _13 = Api.parse(reader, signature: signature) as? Api.Reaction
+ } }
+ var _14: [Int32]?
+ if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() {
+ _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
+ } }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = (Int(_1!) & Int(1 << 18) == 0) || _4 != nil
+ let _c5 = (Int(_1!) & Int(1 << 17) == 0) || _5 != nil
+ let _c6 = _6 != nil
+ let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil
+ let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil
+ let _c9 = _9 != nil
+ let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil
+ let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil
+ let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil
+ let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil
+ let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
+ return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_storyItemDeleted(_ reader: BufferReader) -> StoryItem? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.StoryItem.storyItemDeleted(id: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_storyItemSkipped(_ reader: BufferReader) -> StoryItem? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: Int32?
+ _3 = reader.readInt32()
+ var _4: Int32?
+ _4 = reader.readInt32()
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ if _c1 && _c2 && _c3 && _c4 {
+ return Api.StoryItem.storyItemSkipped(flags: _1!, id: _2!, date: _3!, expireDate: _4!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api {
indirect enum StoryReaction: TypeConstructorDescription {
case storyReaction(peerId: Api.Peer, date: Int32, reaction: Api.Reaction)
diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift
index f48b75e2b2..17bc8c5a9a 100644
--- a/submodules/TelegramApi/Sources/Api29.swift
+++ b/submodules/TelegramApi/Sources/Api29.swift
@@ -1,6 +1,6 @@
public extension Api {
indirect enum WebPageAttribute: TypeConstructorDescription {
- case webPageAttributeStarGiftAuction(gift: Api.StarGift, endDate: Int32, centerColor: Int32, edgeColor: Int32, textColor: Int32)
+ case webPageAttributeStarGiftAuction(gift: Api.StarGift, endDate: Int32)
case webPageAttributeStarGiftCollection(icons: [Api.Document])
case webPageAttributeStickerSet(flags: Int32, stickers: [Api.Document])
case webPageAttributeStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?)
@@ -9,15 +9,12 @@ public extension Api {
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .webPageAttributeStarGiftAuction(let gift, let endDate, let centerColor, let edgeColor, let textColor):
+ case .webPageAttributeStarGiftAuction(let gift, let endDate):
if boxed {
- buffer.appendInt32(55150251)
+ buffer.appendInt32(29770178)
}
gift.serialize(buffer, true)
serializeInt32(endDate, buffer: buffer, boxed: false)
- serializeInt32(centerColor, buffer: buffer, boxed: false)
- serializeInt32(edgeColor, buffer: buffer, boxed: false)
- serializeInt32(textColor, buffer: buffer, boxed: false)
break
case .webPageAttributeStarGiftCollection(let icons):
if boxed {
@@ -72,8 +69,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .webPageAttributeStarGiftAuction(let gift, let endDate, let centerColor, let edgeColor, let textColor):
- return ("webPageAttributeStarGiftAuction", [("gift", gift as Any), ("endDate", endDate as Any), ("centerColor", centerColor as Any), ("edgeColor", edgeColor as Any), ("textColor", textColor as Any)])
+ case .webPageAttributeStarGiftAuction(let gift, let endDate):
+ return ("webPageAttributeStarGiftAuction", [("gift", gift as Any), ("endDate", endDate as Any)])
case .webPageAttributeStarGiftCollection(let icons):
return ("webPageAttributeStarGiftCollection", [("icons", icons as Any)])
case .webPageAttributeStickerSet(let flags, let stickers):
@@ -94,19 +91,10 @@ public extension Api {
}
var _2: Int32?
_2 = reader.readInt32()
- var _3: Int32?
- _3 = reader.readInt32()
- var _4: Int32?
- _4 = reader.readInt32()
- var _5: Int32?
- _5 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- let _c5 = _5 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 {
- return Api.WebPageAttribute.webPageAttributeStarGiftAuction(gift: _1!, endDate: _2!, centerColor: _3!, edgeColor: _4!, textColor: _5!)
+ if _c1 && _c2 {
+ return Api.WebPageAttribute.webPageAttributeStarGiftAuction(gift: _1!, endDate: _2!)
}
else {
return nil
@@ -936,6 +924,86 @@ public extension Api.account {
}
}
+public extension Api.account {
+ enum PasskeyRegistrationOptions: TypeConstructorDescription {
+ case passkeyRegistrationOptions(options: Api.DataJSON)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .passkeyRegistrationOptions(let options):
+ if boxed {
+ buffer.appendInt32(-513057567)
+ }
+ options.serialize(buffer, true)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .passkeyRegistrationOptions(let options):
+ return ("passkeyRegistrationOptions", [("options", options as Any)])
+ }
+ }
+
+ public static func parse_passkeyRegistrationOptions(_ reader: BufferReader) -> PasskeyRegistrationOptions? {
+ var _1: Api.DataJSON?
+ if let signature = reader.readInt32() {
+ _1 = Api.parse(reader, signature: signature) as? Api.DataJSON
+ }
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.account.PasskeyRegistrationOptions.passkeyRegistrationOptions(options: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
+public extension Api.account {
+ enum Passkeys: TypeConstructorDescription {
+ case passkeys(passkeys: [Api.Passkey])
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .passkeys(let passkeys):
+ if boxed {
+ buffer.appendInt32(-119494116)
+ }
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(passkeys.count))
+ for item in passkeys {
+ item.serialize(buffer, true)
+ }
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .passkeys(let passkeys):
+ return ("passkeys", [("passkeys", passkeys as Any)])
+ }
+ }
+
+ public static func parse_passkeys(_ reader: BufferReader) -> Passkeys? {
+ var _1: [Api.Passkey]?
+ if let _ = reader.readInt32() {
+ _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Passkey.self)
+ }
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.account.Passkeys.passkeys(passkeys: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api.account {
enum Password: TypeConstructorDescription {
case password(flags: Int32, currentAlgo: Api.PasswordKdfAlgo?, srpB: Buffer?, srpId: Int64?, hint: String?, emailUnconfirmedPattern: String?, newAlgo: Api.PasswordKdfAlgo, newSecureAlgo: Api.SecurePasswordKdfAlgo, secureRandom: Buffer, pendingResetDate: Int32?, loginEmailPattern: String?)
@@ -1330,57 +1398,3 @@ public extension Api.account {
}
}
-public extension Api.account {
- enum SavedMusicIds: TypeConstructorDescription {
- case savedMusicIds(ids: [Int64])
- case savedMusicIdsNotModified
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .savedMusicIds(let ids):
- if boxed {
- buffer.appendInt32(-1718786506)
- }
- buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(ids.count))
- for item in ids {
- serializeInt64(item, buffer: buffer, boxed: false)
- }
- break
- case .savedMusicIdsNotModified:
- if boxed {
- buffer.appendInt32(1338514798)
- }
-
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .savedMusicIds(let ids):
- return ("savedMusicIds", [("ids", ids as Any)])
- case .savedMusicIdsNotModified:
- return ("savedMusicIdsNotModified", [])
- }
- }
-
- public static func parse_savedMusicIds(_ reader: BufferReader) -> SavedMusicIds? {
- var _1: [Int64]?
- if let _ = reader.readInt32() {
- _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
- }
- let _c1 = _1 != nil
- if _c1 {
- return Api.account.SavedMusicIds.savedMusicIds(ids: _1!)
- }
- else {
- return nil
- }
- }
- public static func parse_savedMusicIdsNotModified(_ reader: BufferReader) -> SavedMusicIds? {
- return Api.account.SavedMusicIds.savedMusicIdsNotModified
- }
-
- }
-}
diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift
index de90e1dd0d..0d25527d8c 100644
--- a/submodules/TelegramApi/Sources/Api30.swift
+++ b/submodules/TelegramApi/Sources/Api30.swift
@@ -1,3 +1,57 @@
+public extension Api.account {
+ enum SavedMusicIds: TypeConstructorDescription {
+ case savedMusicIds(ids: [Int64])
+ case savedMusicIdsNotModified
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .savedMusicIds(let ids):
+ if boxed {
+ buffer.appendInt32(-1718786506)
+ }
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(ids.count))
+ for item in ids {
+ serializeInt64(item, buffer: buffer, boxed: false)
+ }
+ break
+ case .savedMusicIdsNotModified:
+ if boxed {
+ buffer.appendInt32(1338514798)
+ }
+
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .savedMusicIds(let ids):
+ return ("savedMusicIds", [("ids", ids as Any)])
+ case .savedMusicIdsNotModified:
+ return ("savedMusicIdsNotModified", [])
+ }
+ }
+
+ public static func parse_savedMusicIds(_ reader: BufferReader) -> SavedMusicIds? {
+ var _1: [Int64]?
+ if let _ = reader.readInt32() {
+ _1 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
+ }
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.account.SavedMusicIds.savedMusicIds(ids: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+ public static func parse_savedMusicIdsNotModified(_ reader: BufferReader) -> SavedMusicIds? {
+ return Api.account.SavedMusicIds.savedMusicIdsNotModified
+ }
+
+ }
+}
public extension Api.account {
enum SavedRingtone: TypeConstructorDescription {
case savedRingtone
@@ -712,6 +766,44 @@ public extension Api.auth {
}
}
+public extension Api.auth {
+ enum PasskeyLoginOptions: TypeConstructorDescription {
+ case passkeyLoginOptions(options: Api.DataJSON)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .passkeyLoginOptions(let options):
+ if boxed {
+ buffer.appendInt32(-503089271)
+ }
+ options.serialize(buffer, true)
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .passkeyLoginOptions(let options):
+ return ("passkeyLoginOptions", [("options", options as Any)])
+ }
+ }
+
+ public static func parse_passkeyLoginOptions(_ reader: BufferReader) -> PasskeyLoginOptions? {
+ var _1: Api.DataJSON?
+ if let signature = reader.readInt32() {
+ _1 = Api.parse(reader, signature: signature) as? Api.DataJSON
+ }
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.auth.PasskeyLoginOptions.passkeyLoginOptions(options: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api.auth {
enum PasswordRecovery: TypeConstructorDescription {
case passwordRecovery(emailPattern: String)
diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift
index a6764c57a5..22436f1ec2 100644
--- a/submodules/TelegramApi/Sources/Api36.swift
+++ b/submodules/TelegramApi/Sources/Api36.swift
@@ -278,14 +278,14 @@ public extension Api.payments {
}
public extension Api.payments {
enum StarGiftActiveAuctions: TypeConstructorDescription {
- case starGiftActiveAuctions(auctions: [Api.StarGiftActiveAuctionState], users: [Api.User])
+ case starGiftActiveAuctions(auctions: [Api.StarGiftActiveAuctionState], users: [Api.User], chats: [Api.Chat])
case starGiftActiveAuctionsNotModified
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .starGiftActiveAuctions(let auctions, let users):
+ case .starGiftActiveAuctions(let auctions, let users, let chats):
if boxed {
- buffer.appendInt32(-1745778728)
+ buffer.appendInt32(-1359565892)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(auctions.count))
@@ -297,6 +297,11 @@ public extension Api.payments {
for item in users {
item.serialize(buffer, true)
}
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(chats.count))
+ for item in chats {
+ item.serialize(buffer, true)
+ }
break
case .starGiftActiveAuctionsNotModified:
if boxed {
@@ -309,8 +314,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .starGiftActiveAuctions(let auctions, let users):
- return ("starGiftActiveAuctions", [("auctions", auctions as Any), ("users", users as Any)])
+ case .starGiftActiveAuctions(let auctions, let users, let chats):
+ return ("starGiftActiveAuctions", [("auctions", auctions as Any), ("users", users as Any), ("chats", chats as Any)])
case .starGiftActiveAuctionsNotModified:
return ("starGiftActiveAuctionsNotModified", [])
}
@@ -325,10 +330,15 @@ public extension Api.payments {
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
+ var _3: [Api.Chat]?
+ if let _ = reader.readInt32() {
+ _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
+ }
let _c1 = _1 != nil
let _c2 = _2 != nil
- if _c1 && _c2 {
- return Api.payments.StarGiftActiveAuctions.starGiftActiveAuctions(auctions: _1!, users: _2!)
+ let _c3 = _3 != nil
+ if _c1 && _c2 && _c3 {
+ return Api.payments.StarGiftActiveAuctions.starGiftActiveAuctions(auctions: _1!, users: _2!, chats: _3!)
}
else {
return nil
@@ -404,13 +414,13 @@ public extension Api.payments {
}
public extension Api.payments {
enum StarGiftAuctionState: TypeConstructorDescription {
- case starGiftAuctionState(gift: Api.StarGift, state: Api.StarGiftAuctionState, userState: Api.StarGiftAuctionUserState, timeout: Int32, users: [Api.User])
+ case starGiftAuctionState(gift: Api.StarGift, state: Api.StarGiftAuctionState, userState: Api.StarGiftAuctionUserState, timeout: Int32, users: [Api.User], chats: [Api.Chat])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
- case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users):
+ case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users, let chats):
if boxed {
- buffer.appendInt32(244900980)
+ buffer.appendInt32(1798960364)
}
gift.serialize(buffer, true)
state.serialize(buffer, true)
@@ -421,14 +431,19 @@ public extension Api.payments {
for item in users {
item.serialize(buffer, true)
}
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(chats.count))
+ for item in chats {
+ item.serialize(buffer, true)
+ }
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
- case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users):
- return ("starGiftAuctionState", [("gift", gift as Any), ("state", state as Any), ("userState", userState as Any), ("timeout", timeout as Any), ("users", users as Any)])
+ case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users, let chats):
+ return ("starGiftAuctionState", [("gift", gift as Any), ("state", state as Any), ("userState", userState as Any), ("timeout", timeout as Any), ("users", users as Any), ("chats", chats as Any)])
}
}
@@ -451,13 +466,18 @@ public extension Api.payments {
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
+ var _6: [Api.Chat]?
+ if let _ = reader.readInt32() {
+ _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
+ }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 {
- return Api.payments.StarGiftAuctionState.starGiftAuctionState(gift: _1!, state: _2!, userState: _3!, timeout: _4!, users: _5!)
+ let _c6 = _6 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
+ return Api.payments.StarGiftAuctionState.starGiftAuctionState(gift: _1!, state: _2!, userState: _3!, timeout: _4!, users: _5!, chats: _6!)
}
else {
return nil
@@ -520,6 +540,48 @@ public extension Api.payments {
}
}
+public extension Api.payments {
+ enum StarGiftUpgradeAttributes: TypeConstructorDescription {
+ case starGiftUpgradeAttributes(attributes: [Api.StarGiftAttribute])
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .starGiftUpgradeAttributes(let attributes):
+ if boxed {
+ buffer.appendInt32(1187439471)
+ }
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(attributes.count))
+ for item in attributes {
+ item.serialize(buffer, true)
+ }
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .starGiftUpgradeAttributes(let attributes):
+ return ("starGiftUpgradeAttributes", [("attributes", attributes as Any)])
+ }
+ }
+
+ public static func parse_starGiftUpgradeAttributes(_ reader: BufferReader) -> StarGiftUpgradeAttributes? {
+ var _1: [Api.StarGiftAttribute]?
+ if let _ = reader.readInt32() {
+ _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self)
+ }
+ let _c1 = _1 != nil
+ if _c1 {
+ return Api.payments.StarGiftUpgradeAttributes.starGiftUpgradeAttributes(attributes: _1!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api.payments {
enum StarGiftUpgradePreview: TypeConstructorDescription {
case starGiftUpgradePreview(sampleAttributes: [Api.StarGiftAttribute], prices: [Api.StarGiftUpgradePrice], nextPrices: [Api.StarGiftUpgradePrice])
@@ -1616,51 +1678,3 @@ public extension Api.phone {
}
}
-public extension Api.photos {
- enum Photo: TypeConstructorDescription {
- case photo(photo: Api.Photo, users: [Api.User])
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .photo(let photo, let users):
- if boxed {
- buffer.appendInt32(539045032)
- }
- photo.serialize(buffer, true)
- buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(users.count))
- for item in users {
- item.serialize(buffer, true)
- }
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .photo(let photo, let users):
- return ("photo", [("photo", photo as Any), ("users", users as Any)])
- }
- }
-
- public static func parse_photo(_ reader: BufferReader) -> Photo? {
- var _1: Api.Photo?
- if let signature = reader.readInt32() {
- _1 = Api.parse(reader, signature: signature) as? Api.Photo
- }
- var _2: [Api.User]?
- if let _ = reader.readInt32() {
- _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
- }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- if _c1 && _c2 {
- return Api.photos.Photo.photo(photo: _1!, users: _2!)
- }
- else {
- return nil
- }
- }
-
- }
-}
diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift
index 74ec103224..3b5d24155d 100644
--- a/submodules/TelegramApi/Sources/Api37.swift
+++ b/submodules/TelegramApi/Sources/Api37.swift
@@ -1,3 +1,51 @@
+public extension Api.photos {
+ enum Photo: TypeConstructorDescription {
+ case photo(photo: Api.Photo, users: [Api.User])
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .photo(let photo, let users):
+ if boxed {
+ buffer.appendInt32(539045032)
+ }
+ photo.serialize(buffer, true)
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(users.count))
+ for item in users {
+ item.serialize(buffer, true)
+ }
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .photo(let photo, let users):
+ return ("photo", [("photo", photo as Any), ("users", users as Any)])
+ }
+ }
+
+ public static func parse_photo(_ reader: BufferReader) -> Photo? {
+ var _1: Api.Photo?
+ if let signature = reader.readInt32() {
+ _1 = Api.parse(reader, signature: signature) as? Api.Photo
+ }
+ var _2: [Api.User]?
+ if let _ = reader.readInt32() {
+ _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
+ }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ if _c1 && _c2 {
+ return Api.photos.Photo.photo(photo: _1!, users: _2!)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api.photos {
enum Photos: TypeConstructorDescription {
case photos(photos: [Api.Photo], users: [Api.User])
@@ -1474,77 +1522,3 @@ public extension Api.stories {
}
}
-public extension Api.stories {
- enum StoryReactionsList: TypeConstructorDescription {
- case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?)
-
- public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
- switch self {
- case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
- if boxed {
- buffer.appendInt32(-1436583780)
- }
- serializeInt32(flags, buffer: buffer, boxed: false)
- serializeInt32(count, buffer: buffer, boxed: false)
- buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(reactions.count))
- for item in reactions {
- item.serialize(buffer, true)
- }
- buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(chats.count))
- for item in chats {
- item.serialize(buffer, true)
- }
- buffer.appendInt32(481674261)
- buffer.appendInt32(Int32(users.count))
- for item in users {
- item.serialize(buffer, true)
- }
- if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
- break
- }
- }
-
- public func descriptionFields() -> (String, [(String, Any)]) {
- switch self {
- case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
- return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)])
- }
- }
-
- public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? {
- var _1: Int32?
- _1 = reader.readInt32()
- var _2: Int32?
- _2 = reader.readInt32()
- var _3: [Api.StoryReaction]?
- if let _ = reader.readInt32() {
- _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self)
- }
- var _4: [Api.Chat]?
- if let _ = reader.readInt32() {
- _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
- }
- var _5: [Api.User]?
- if let _ = reader.readInt32() {
- _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
- }
- var _6: String?
- if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
- let _c1 = _1 != nil
- let _c2 = _2 != nil
- let _c3 = _3 != nil
- let _c4 = _4 != nil
- let _c5 = _5 != nil
- let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
- if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
- return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)
- }
- else {
- return nil
- }
- }
-
- }
-}
diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift
index f5fe6ba904..77f635e8bb 100644
--- a/submodules/TelegramApi/Sources/Api38.swift
+++ b/submodules/TelegramApi/Sources/Api38.swift
@@ -1,3 +1,77 @@
+public extension Api.stories {
+ enum StoryReactionsList: TypeConstructorDescription {
+ case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?)
+
+ public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
+ switch self {
+ case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
+ if boxed {
+ buffer.appendInt32(-1436583780)
+ }
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(count, buffer: buffer, boxed: false)
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(reactions.count))
+ for item in reactions {
+ item.serialize(buffer, true)
+ }
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(chats.count))
+ for item in chats {
+ item.serialize(buffer, true)
+ }
+ buffer.appendInt32(481674261)
+ buffer.appendInt32(Int32(users.count))
+ for item in users {
+ item.serialize(buffer, true)
+ }
+ if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)}
+ break
+ }
+ }
+
+ public func descriptionFields() -> (String, [(String, Any)]) {
+ switch self {
+ case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset):
+ return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)])
+ }
+ }
+
+ public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? {
+ var _1: Int32?
+ _1 = reader.readInt32()
+ var _2: Int32?
+ _2 = reader.readInt32()
+ var _3: [Api.StoryReaction]?
+ if let _ = reader.readInt32() {
+ _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self)
+ }
+ var _4: [Api.Chat]?
+ if let _ = reader.readInt32() {
+ _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
+ }
+ var _5: [Api.User]?
+ if let _ = reader.readInt32() {
+ _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
+ }
+ var _6: String?
+ if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
+ let _c1 = _1 != nil
+ let _c2 = _2 != nil
+ let _c3 = _3 != nil
+ let _c4 = _4 != nil
+ let _c5 = _5 != nil
+ let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
+ if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
+ return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)
+ }
+ else {
+ return nil
+ }
+ }
+
+ }
+}
public extension Api.stories {
enum StoryViews: TypeConstructorDescription {
case storyViews(views: [Api.StoryViews], users: [Api.User])
diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift
index afd911cfe6..d421b4d691 100644
--- a/submodules/TelegramApi/Sources/Api39.swift
+++ b/submodules/TelegramApi/Sources/Api39.swift
@@ -232,6 +232,21 @@ public extension Api.functions.account {
})
}
}
+public extension Api.functions.account {
+ static func deletePasskey(id: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(-172665281)
+ serializeString(id, buffer: buffer, boxed: false)
+ return (FunctionDescription(name: "account.deletePasskey", parameters: [("id", String(describing: id))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
+ let reader = BufferReader(buffer)
+ var result: Api.Bool?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.Bool
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.account {
static func deleteSecureValue(types: [Api.SecureValueType]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -666,6 +681,21 @@ public extension Api.functions.account {
})
}
}
+public extension Api.functions.account {
+ static func getPasskeys() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(-367063982)
+
+ return (FunctionDescription(name: "account.getPasskeys", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.Passkeys? in
+ let reader = BufferReader(buffer)
+ var result: Api.account.Passkeys?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.account.Passkeys
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.account {
static func getPassword() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -900,6 +930,21 @@ public extension Api.functions.account {
})
}
}
+public extension Api.functions.account {
+ static func initPasskeyRegistration() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(1117079528)
+
+ return (FunctionDescription(name: "account.initPasskeyRegistration", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.account.PasskeyRegistrationOptions? in
+ let reader = BufferReader(buffer)
+ var result: Api.account.PasskeyRegistrationOptions?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.account.PasskeyRegistrationOptions
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.account {
static func initTakeoutSession(flags: Int32, fileMaxSize: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -993,6 +1038,21 @@ public extension Api.functions.account {
})
}
}
+public extension Api.functions.account {
+ static func registerPasskey(credential: Api.InputPasskeyCredential) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(1437867990)
+ credential.serialize(buffer, true)
+ return (FunctionDescription(name: "account.registerPasskey", parameters: [("credential", String(describing: credential))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Passkey? in
+ let reader = BufferReader(buffer)
+ var result: Api.Passkey?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.Passkey
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.account {
static func reorderUsernames(order: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -2075,6 +2135,24 @@ public extension Api.functions.auth {
})
}
}
+public extension Api.functions.auth {
+ static func finishPasskeyLogin(flags: Int32, credential: Api.InputPasskeyCredential, fromDcId: Int32?, fromAuthKeyId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(-1739084537)
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ credential.serialize(buffer, true)
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt32(fromDcId!, buffer: buffer, boxed: false)}
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt64(fromAuthKeyId!, buffer: buffer, boxed: false)}
+ return (FunctionDescription(name: "auth.finishPasskeyLogin", parameters: [("flags", String(describing: flags)), ("credential", String(describing: credential)), ("fromDcId", String(describing: fromDcId)), ("fromAuthKeyId", String(describing: fromAuthKeyId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.Authorization? in
+ let reader = BufferReader(buffer)
+ var result: Api.auth.Authorization?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.auth.Authorization
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.auth {
static func importAuthorization(id: Int64, bytes: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -2141,6 +2219,22 @@ public extension Api.functions.auth {
})
}
}
+public extension Api.functions.auth {
+ static func initPasskeyLogin(apiId: Int32, apiHash: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(1368051895)
+ serializeInt32(apiId, buffer: buffer, boxed: false)
+ serializeString(apiHash, buffer: buffer, boxed: false)
+ return (FunctionDescription(name: "auth.initPasskeyLogin", parameters: [("apiId", String(describing: apiId)), ("apiHash", String(describing: apiHash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.auth.PasskeyLoginOptions? in
+ let reader = BufferReader(buffer)
+ var result: Api.auth.PasskeyLoginOptions?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.auth.PasskeyLoginOptions
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.auth {
static func logOut() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -5736,9 +5830,9 @@ public extension Api.functions.messages {
}
}
public extension Api.functions.messages {
- static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, scheduleRepeatPeriod: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, videoTimestamp: Int32?, allowPaidStars: Int64?, suggestedPost: Api.SuggestedPost?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, scheduleRepeatPeriod: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, effect: Int64?, videoTimestamp: Int32?, allowPaidStars: Int64?, suggestedPost: Api.SuggestedPost?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
- buffer.appendInt32(1104419550)
+ buffer.appendInt32(326126204)
serializeInt32(flags, buffer: buffer, boxed: false)
fromPeer.serialize(buffer, true)
buffer.appendInt32(481674261)
@@ -5758,10 +5852,11 @@ public extension Api.functions.messages {
if Int(flags) & Int(1 << 24) != 0 {serializeInt32(scheduleRepeatPeriod!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)}
if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)}
+ if Int(flags) & Int(1 << 18) != 0 {serializeInt64(effect!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 20) != 0 {serializeInt32(videoTimestamp!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 21) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 23) != 0 {suggestedPost!.serialize(buffer, true)}
- return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("scheduleRepeatPeriod", String(describing: scheduleRepeatPeriod)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars)), ("suggestedPost", String(describing: suggestedPost))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
+ return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("scheduleRepeatPeriod", String(describing: scheduleRepeatPeriod)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("effect", String(describing: effect)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars)), ("suggestedPost", String(describing: suggestedPost))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
@@ -9816,6 +9911,21 @@ public extension Api.functions.payments {
})
}
}
+public extension Api.functions.payments {
+ static func getStarGiftUpgradeAttributes(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(1828948824)
+ serializeInt64(giftId, buffer: buffer, boxed: false)
+ return (FunctionDescription(name: "payments.getStarGiftUpgradeAttributes", parameters: [("giftId", String(describing: giftId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarGiftUpgradeAttributes? in
+ let reader = BufferReader(buffer)
+ var result: Api.payments.StarGiftUpgradeAttributes?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.payments.StarGiftUpgradeAttributes
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.payments {
static func getStarGiftUpgradePreview(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -10131,6 +10241,22 @@ public extension Api.functions.payments {
})
}
}
+public extension Api.functions.payments {
+ static func resolveStarGiftOffer(flags: Int32, offerMsgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(-372344804)
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ serializeInt32(offerMsgId, buffer: buffer, boxed: false)
+ return (FunctionDescription(name: "payments.resolveStarGiftOffer", parameters: [("flags", String(describing: flags)), ("offerMsgId", String(describing: offerMsgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
+ let reader = BufferReader(buffer)
+ var result: Api.Updates?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.Updates
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.payments {
static func saveStarGift(flags: Int32, stargift: Api.InputSavedStarGift) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
@@ -10168,6 +10294,27 @@ public extension Api.functions.payments {
})
}
}
+public extension Api.functions.payments {
+ static func sendStarGiftOffer(flags: Int32, peer: Api.InputPeer, slug: String, price: Api.StarsAmount, duration: Int32, randomId: Int64, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
+ let buffer = Buffer()
+ buffer.appendInt32(-1883739327)
+ serializeInt32(flags, buffer: buffer, boxed: false)
+ peer.serialize(buffer, true)
+ serializeString(slug, buffer: buffer, boxed: false)
+ price.serialize(buffer, true)
+ serializeInt32(duration, buffer: buffer, boxed: false)
+ serializeInt64(randomId, buffer: buffer, boxed: false)
+ if Int(flags) & Int(1 << 0) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)}
+ return (FunctionDescription(name: "payments.sendStarGiftOffer", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("slug", String(describing: slug)), ("price", String(describing: price)), ("duration", String(describing: duration)), ("randomId", String(describing: randomId)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
+ let reader = BufferReader(buffer)
+ var result: Api.Updates?
+ if let signature = reader.readInt32() {
+ result = Api.parse(reader, signature: signature) as? Api.Updates
+ }
+ return result
+ })
+ }
+}
public extension Api.functions.payments {
static func sendStarsForm(formId: Int64, invoice: Api.InputInvoice) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) {
let buffer = Buffer()
diff --git a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift
index 94a6c208e1..c030d22514 100644
--- a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift
+++ b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift
@@ -103,8 +103,9 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
}
}
- let titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count))
- titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor))))))
+ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
+
+ var titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count))
let subtitleText: String
var subtitleTextColor = self.theme.rootController.navigationBar.secondaryTextColor
@@ -113,8 +114,21 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
if self.states.count == 1, let auctionState = self.states.first {
+ var isUpcoming = false
+ var startTime = currentTime
+ var endTime = currentTime
+ if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState {
+ startTime = startDate
+ endTime = nextRoundDate
+ if currentTime < startDate {
+ isUpcoming = true
+ }
+ }
+
let place = auctionState.place ?? 1
- if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
+ if isUpcoming {
+ subtitleText = self.strings.ChatList_Auctions_Status_UpcomingBid
+ } else if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = self.strings.ChatList_Auctions_Status_Single_Outbid
subtitleTextColor = self.theme.list.itemDestructiveColor
isOutbid = true
@@ -134,14 +148,14 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string
}
- let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
- var endTime = currentTime
- if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState.auctionState {
- endTime = nextRoundDate
+ let endTimeout: Int32
+ if currentTime < startTime {
+ endTimeout = max(0, startTime - currentTime)
+ titleText = self.strings.ChatList_Auctions_UpcomingAuction
+ } else {
+ endTimeout = max(0, endTime - currentTime)
}
- let endTimeout = max(0, endTime - currentTime)
-
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
@@ -179,6 +193,8 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode {
buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "view", content: .text(self.strings.ChatList_Auctions_View)))
}
+ titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor))))))
+
let buttonSize = self.button.update(
transition: .spring(duration: 0.2),
component: AnyComponent(
diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift
index e17ded2bde..262176da07 100644
--- a/submodules/TelegramCore/Sources/Account/Account.swift
+++ b/submodules/TelegramCore/Sources/Account/Account.swift
@@ -340,7 +340,13 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in
return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection))
}
@@ -453,6 +459,131 @@ func _internal_twoStepAuthData(_ network: Network) -> Signal Bool {
+ if lhs.id != rhs.id {
+ return false
+ }
+ if lhs.name != rhs.name {
+ return false
+ }
+ if lhs.date != rhs.date {
+ return false
+ }
+ if lhs.emojiId != rhs.emojiId {
+ return false
+ }
+ if lhs.lastUsageDate != rhs.lastUsageDate {
+ return false
+ }
+ return true
+ }
+}
+
+extension TelegramPasskey {
+ convenience init(apiPasskey: Api.Passkey) {
+ switch apiPasskey {
+ case let .passkey(_, id, name, date, softwareEmojiId, lastUsageDate):
+ self.init(id: id, name: name, date: date, emojiId: softwareEmojiId, lastUsageDate: lastUsageDate)
+ }
+ }
+}
+
+func _internal_passkeysData(network: Network) -> Signal<[TelegramPasskey], NoError> {
+ return network.request(Api.functions.account.getPasskeys())
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> map { passkeys -> [TelegramPasskey] in
+ guard let passkeys else {
+ return []
+ }
+ switch passkeys {
+ case let .passkeys(passkeys):
+ return passkeys.map { passkey in
+ return TelegramPasskey(apiPasskey: passkey)
+ }
+ }
+ }
+}
+
+func _internal_requestPasskeyRegistration(network: Network) -> Signal {
+ return network.request(Api.functions.account.initPasskeyRegistration())
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> map { options -> String? in
+ guard let options else {
+ return nil
+ }
+ switch options {
+ case let .passkeyRegistrationOptions(options):
+ switch options {
+ case let .dataJSON(data):
+ return data
+ }
+ }
+ }
+}
+
+func _internal_requestCreatePasskey(network: Network, id: String, clientData: String, attestationObject: Data) -> Signal {
+ return network.request(Api.functions.account.registerPasskey(credential: .inputPasskeyCredentialPublicKey(id: id, rawId: id, response: .inputPasskeyResponseRegister(clientData: .dataJSON(data: clientData), attestationData: Buffer(data: attestationObject)))))
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> map { result -> TelegramPasskey? in
+ guard let result else {
+ return nil
+ }
+ return TelegramPasskey(apiPasskey: result)
+ }
+}
+
+func _internal_deletePasskey(network: Network, id: String) -> Signal {
+ return network.request(Api.functions.account.deletePasskey(id: id))
+ |> `catch` { _ -> Signal in
+ return .single(.boolFalse)
+ }
+ |> ignoreValues
+}
+
+func _internal_requestPasskeyLoginData(network: Network, apiId: Int32, apiHash: String) -> Signal {
+ return network.request(Api.functions.auth.initPasskeyLogin(apiId: apiId, apiHash: apiHash))
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> map { result -> String? in
+ guard let result else {
+ return nil
+ }
+ switch result {
+ case let .passkeyLoginOptions(options):
+ switch options {
+ case let .dataJSON(data):
+ return data
+ }
+ }
+ }
+}
+
public func hexString(_ data: Data) -> String {
let hexString = NSMutableString()
data.withUnsafeBytes { rawBytes -> Void in
diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift
index b3790e80e2..f32fbbe7b9 100644
--- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift
+++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift
@@ -232,7 +232,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
}
switch action {
- case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks, .messageActionSuggestedPostApproval, .messageActionGiftTon, .messageActionSuggestedPostSuccess, .messageActionSuggestedPostRefund, .messageActionSuggestBirthday:
+ case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks, .messageActionSuggestedPostApproval, .messageActionGiftTon, .messageActionSuggestedPostSuccess, .messageActionSuggestedPostRefund, .messageActionSuggestBirthday, .messageActionStarGiftPurchaseOffer, .messageActionStarGiftPurchaseOfferDeclined:
break
case let .messageActionChannelMigrateFrom(_, chatId):
result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)))
diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
index 8d2c483acf..e92c28725d 100644
--- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
+++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift
@@ -181,7 +181,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId))
case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId):
return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId)))
- case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId):
+ case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId, number):
let text: String?
let entities: [MessageTextEntity]?
switch message {
@@ -195,12 +195,12 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
- return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId))
+ return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId, number: number))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
- return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0))
+ return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0))
case let .messageActionPaidMessagesRefunded(count, stars):
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(flags, stars):
@@ -274,6 +274,17 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .suggestedPostRefund(TelegramMediaActionType.SuggestedPostRefund(isUserInitiated: (flags & (1 << 0)) != 0)))
case let .messageActionSuggestBirthday(birthday):
return TelegramMediaAction(action: .suggestedBirthday(TelegramBirthday(apiBirthday: birthday)))
+
+ case let .messageActionStarGiftPurchaseOffer(flags, apiGift, price, expiresAt):
+ guard let gift = StarGift(apiStarGift: apiGift) else {
+ return nil
+ }
+ return TelegramMediaAction(action: .starGiftPurchaseOffer(gift: gift, amount: CurrencyAmount(apiAmount: price), expireDate: expiresAt, isAccepted: (flags & (1 << 0)) != 0, isDeclined: (flags & (1 << 1)) != 0))
+ case let .messageActionStarGiftPurchaseOfferDeclined(flags, apiGift, price):
+ guard let gift = StarGift(apiStarGift: apiGift) else {
+ return nil
+ }
+ return TelegramMediaAction(action: .starGiftPurchaseOfferDeclined(gift: gift, amount: CurrencyAmount(apiAmount: price), hasExpired: (flags & (1 << 0)) != 0))
}
}
diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift
index b08d3f4d91..02bc803447 100644
--- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift
+++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift
@@ -32,11 +32,11 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa
var files: [TelegramMediaFile] = []
files = icons.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
return .giftCollection(TelegramMediaWebpageGiftCollectionAttribute(files: files))
- case let .webPageAttributeStarGiftAuction(apiGift, endDate, centerColor, edgeColor, textColor):
+ case let .webPageAttributeStarGiftAuction(apiGift, endDate):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
- return .giftAuction(TelegramMediaWebpageGiftAuctionAttribute(gift: gift, endDate: endDate, centerColor: centerColor, edgeColor: edgeColor, textColor: textColor))
+ return .giftAuction(TelegramMediaWebpageGiftAuctionAttribute(gift: gift, endDate: endDate))
case .webPageAttributeStory:
return nil
}
diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift
index 1d6523c759..db7ce4dd78 100644
--- a/submodules/TelegramCore/Sources/Authorization.swift
+++ b/submodules/TelegramCore/Sources/Authorization.swift
@@ -1167,6 +1167,135 @@ public func authorizeWithPassword(accountManager: AccountManager, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, foreignDatacenter: (id: Int, authKeyId: Int64)?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal {
+ let userHandle = passkey.userHandle.components(separatedBy: ":")
+ var targetDatacenterId: Int?
+ if foreignDatacenter == nil && userHandle.count >= 2 {
+ targetDatacenterId = Int(userHandle[0])
+ }
+
+ if let targetDatacenterId, account.masterDatacenterId != Int32(targetDatacenterId) {
+ let initialDatacenterId = account.masterDatacenterId
+ return account.network.getAuthKeyId()
+ |> castError(AuthorizationCodeVerificationError.self)
+ |> mapToSignal { sourceAuthKeyId -> Signal in
+ let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: Int32(targetDatacenterId))
+ return updatedAccount
+ |> mapToSignalPromotingError { updatedAccount -> Signal in
+ return authorizeWithPasskey(accountManager: accountManager, account: updatedAccount, passkey: passkey, foreignDatacenter: (Int(initialDatacenterId), sourceAuthKeyId), forcedPasswordSetupNotice: forcedPasswordSetupNotice, syncContacts: syncContacts)
+ }
+ }
+ }
+
+ var flags: Int32 = 0
+ if foreignDatacenter != nil {
+ flags |= 1 << 0
+ }
+ return account.network.request(Api.functions.auth.finishPasskeyLogin(flags: flags, credential: .inputPasskeyCredentialPublicKey(id: passkey.id, rawId: passkey.id, response: .inputPasskeyResponseLogin(clientData: .dataJSON(data: passkey.clientData), authenticatorData: Buffer(data: passkey.authenticatorData), signature: Buffer(data: passkey.signature), userHandle: passkey.userHandle)), fromDcId: (foreignDatacenter?.id).flatMap(Int32.init), fromAuthKeyId: foreignDatacenter?.authKeyId), automaticFloodWait: false)
+ |> map { authorization in
+ return .authorization(authorization)
+ }
+ |> `catch` { error -> Signal in
+ switch (error.errorCode, error.errorDescription ?? "") {
+ case (401, "SESSION_PASSWORD_NEEDED"):
+ return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false)
+ |> mapError { error -> AuthorizationCodeVerificationError in
+ if error.errorDescription.hasPrefix("FLOOD_WAIT") {
+ return .limitExceeded
+ } else {
+ return .generic
+ }
+ }
+ |> mapToSignal { result -> Signal in
+ switch result {
+ case let .password(_, _, _, _, hint, _, _, _, _, _, _):
+ return .single(.password(hint: hint ?? ""))
+ }
+ }
+ case let (_, errorDescription):
+ if errorDescription.hasPrefix("FLOOD_WAIT") {
+ return .fail(.limitExceeded)
+ } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" {
+ return .fail(.invalidCode)
+ } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" {
+ return .fail(.codeExpired)
+ } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" {
+ return .single(.signUp)
+ } else if errorDescription == "EMAIL_TOKEN_INVALID" {
+ return .fail(.invalidEmailToken)
+ } else if errorDescription == "EMAIL_ADDRESS_INVALID" {
+ return .fail(.invalidEmailAddress)
+ } else {
+ return .fail(.generic)
+ }
+ }
+ }
+ |> mapToSignal { result -> Signal in
+ return account.postbox.transaction { transaction -> Signal in
+ switch result {
+ case .signUp:
+ return .fail(.generic)
+ case let .password(hint):
+ transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: nil, code: nil, suggestReset: false, syncContacts: syncContacts)))
+ return .single(AuthorizeWithPasskeyResult(updatedAccount: account))
+ case let .authorization(authorization):
+ switch authorization {
+ case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user):
+ if let futureAuthToken = futureAuthToken {
+ storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
+ }
+
+ let user = TelegramUser(user: user)
+ var isSupportUser = false
+ if let phone = user.phone, phone.hasPrefix("42") {
+ isSupportUser = true
+ }
+ let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
+ initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
+ transaction.setState(state)
+ if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
+ transaction.setNoticeEntry(key: value.0, value: value.1)
+ }
+ return accountManager.transaction { transaction -> AuthorizeWithPasskeyResult in
+ switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
+ return AuthorizeWithPasskeyResult(updatedAccount: account)
+ }
+ |> castError(AuthorizationCodeVerificationError.self)
+ case .authorizationSignUpRequired:
+ return .fail(.generic)
+ }
+ }
+ }
+ |> mapError { _ -> AuthorizationCodeVerificationError in
+ }
+ |> switchToLatest
+ }
+}
+
public enum PasswordRecoveryRequestError {
case limitExceeded
case generic
@@ -1466,3 +1595,4 @@ func _internal_reportMissingCode(network: Network, phoneNumber: String, phoneCod
return .complete()
}
}
+
diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift
index f9dd1d6c1e..75e53aa7b6 100644
--- a/submodules/TelegramCore/Sources/Network/Network.swift
+++ b/submodules/TelegramCore/Sources/Network/Network.swift
@@ -1065,6 +1065,22 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
}
}
+ public func getAuthKeyId() -> Signal {
+ let mtContext = self.mtProto.context
+ let datacenterId = self.datacenterId
+ return Signal { subscriber in
+ MTContext.contextQueue().dispatch(onQueue: {
+ var result: Int64 = 0
+ if let authInfo = mtContext?.authInfoForDatacenter(withId: datacenterId, selector: .persistent) {
+ result = authInfo.authKeyId
+ }
+ subscriber.putNext(result)
+ })
+
+ return EmptyDisposable
+ }
+ }
+
public func requestWithAdditionalInfo(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal, MTRpcError> {
let requestService = self.requestService
return Signal { subscriber in
diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift
index 387b022a43..c4a52df96a 100644
--- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift
+++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift
@@ -499,7 +499,7 @@ private func sendUploadedMessageContent(
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
- sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag)
+ sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))
diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift
index bbca2809d5..4a954b1c76 100644
--- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift
+++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift
@@ -360,10 +360,12 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil,
+ valueUsdAmount: nil,
flags: [],
themePeerId: nil,
peerColor: nil,
- hostPeerId: nil
+ hostPeerId: nil,
+ minOfferStars: nil
)
if let entry = CodableEntry(RecentStarGiftItem(gift)) {
items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry))
diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift
index 42c4920826..cbdf0a3a1b 100644
--- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift
+++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift
@@ -1133,7 +1133,7 @@ public final class PendingMessageManager {
} else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) {
let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id)
- sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
+ sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
} else {
assertionFailure()
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source"))
@@ -1831,7 +1831,7 @@ public final class PendingMessageManager {
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
- sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
+ sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))
diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift
index cc3b70c115..0a8680497f 100644
--- a/submodules/TelegramCore/Sources/State/Serialization.swift
+++ b/submodules/TelegramCore/Sources/State/Serialization.swift
@@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
- return 218
+ return 220
}
public func parseMessage(_ data: Data!) -> Any! {
diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift
index 041af82f71..d6b47a9a37 100644
--- a/submodules/TelegramCore/Sources/Suggestions.swift
+++ b/submodules/TelegramCore/Sources/Suggestions.swift
@@ -20,6 +20,7 @@ public enum ServerProvidedSuggestion: Equatable {
case setupPhoto
case setupLoginEmail
case setupLoginEmailBlocking
+ case setupPasskey
case link(id: String, url: String, title: ServerSuggestionInfo.Item.Text, subtitle: ServerSuggestionInfo.Item.Text)
init?(string: String) {
@@ -56,6 +57,8 @@ public enum ServerProvidedSuggestion: Equatable {
self = .setupLoginEmail
case "SETUP_LOGIN_EMAIL_NOSKIP":
self = .setupLoginEmailBlocking
+ case "SETUP_PASSKEY":
+ self = .setupPasskey
default:
return nil
}
@@ -95,6 +98,8 @@ public enum ServerProvidedSuggestion: Equatable {
return "SETUP_LOGIN_EMAIL"
case .setupLoginEmailBlocking:
return "SETUP_LOGIN_EMAIL_NOSKIP"
+ case .setupPasskey:
+ return "SETUP_PASSKEY"
case let .link(id, _, _, _):
return id
}
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift
index c81fbbf369..b6a82d2f10 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift
@@ -148,6 +148,7 @@ public struct Namespaces {
public static let cachedProfileSavedMusic: Int8 = 49
public static let cachedChatThemes: Int8 = 50
public static let cachedLiveStorySendAsPeers: Int8 = 51
+ public static let cachedGiftUpgradesAttributes: Int8 = 52
}
public struct UnorderedItemList {
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
index 549e5dd77a..f65aaeded5 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift
@@ -255,8 +255,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String)
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
- case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?)
- case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool)
+ case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?, number: Int32?)
+ case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool)
case conferenceCall(ConferenceCall)
@@ -267,6 +267,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case suggestedPostSuccess(amount: CurrencyAmount)
case suggestedPostRefund(SuggestedPostRefund)
case suggestedBirthday(TelegramBirthday)
+ case starGiftPurchaseOffer(gift: StarGift, amount: CurrencyAmount, expireDate: Int32, isAccepted: Bool, isDeclined: Bool)
+ case starGiftPurchaseOfferDeclined(gift: StarGift, amount: CurrencyAmount, hasExpired: Bool)
public init(decoder: PostboxDecoder) {
let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0)
@@ -394,7 +396,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
}
self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId)
case 44:
- self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false, isAuctionAcquired: decoder.decodeOptionalBoolForKey("isAuctionAcquired") ?? false, toPeerId: decoder.decodeOptionalInt64ForKey("toPeerId").flatMap { EnginePeer.Id($0) })
+ self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false, isAuctionAcquired: decoder.decodeOptionalBoolForKey("isAuctionAcquired") ?? false, toPeerId: decoder.decodeOptionalInt64ForKey("toPeerId").flatMap { EnginePeer.Id($0) }, number: decoder.decodeOptionalInt32ForKey("number"))
case 45:
var resaleAmount: CurrencyAmount?
if let amount = decoder.decodeCodable(CurrencyAmount.self, forKey: "resaleAmount") {
@@ -402,7 +404,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") {
resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars)
}
- self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false))
+ self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false))
case 46:
self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 47:
@@ -434,6 +436,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
self = .suggestedPostRefund(decoder.decodeCodable(SuggestedPostRefund.self, forKey: "s") ?? SuggestedPostRefund(isUserInitiated: true))
case 55:
self = .suggestedBirthday(decoder.decodeCodable(TelegramBirthday.self, forKey: "birthday") ?? TelegramBirthday(day: 1, month: 1, year: nil))
+ case 56:
+ self = .starGiftPurchaseOffer(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount")!, expireDate: decoder.decodeInt32ForKey("expireDate", orElse: 0), isAccepted: decoder.decodeBoolForKey("isAccepted", orElse: false), isDeclined: decoder.decodeBoolForKey("isDeclined", orElse: false))
+ case 57:
+ self = .starGiftPurchaseOfferDeclined(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount")!, hasExpired: decoder.decodeBoolForKey("hasExpired", orElse: false))
default:
self = .unknown
}
@@ -728,7 +734,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "giveawayMsgId")
}
- case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, isAuctionAcquired, toPeerId):
+ case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, isAuctionAcquired, toPeerId, number):
encoder.encodeInt32(44, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
if let convertStars {
@@ -792,7 +798,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "toPeerId")
}
- case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned):
+ if let number {
+ encoder.encodeInt32(number, forKey: "number")
+ } else {
+ encoder.encodeNil(forKey: "number")
+ }
+ case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
encoder.encodeBool(isUpgrade, forKey: "isUpgrade")
@@ -846,6 +857,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeNil(forKey: "dropOriginalDetailsStars")
}
encoder.encodeBool(assigned, forKey: "assigned")
+ encoder.encodeBool(fromOffer, forKey: "fromOffer")
case let .paidMessagesRefunded(count, stars):
encoder.encodeInt32(46, forKey: "_rawValue")
encoder.encodeInt32(count, forKey: "count")
@@ -899,6 +911,18 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .suggestedBirthday(birthday):
encoder.encodeInt32(55, forKey: "_rawValue")
encoder.encodeCodable(birthday, forKey: "birthday")
+ case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined):
+ encoder.encodeInt32(56, forKey: "_rawValue")
+ encoder.encodeObject(gift, forKey: "gift")
+ encoder.encodeCodable(amount, forKey: "amount")
+ encoder.encodeInt32(expireDate, forKey: "expireDate")
+ encoder.encodeBool(isAccepted, forKey: "isAccepted")
+ encoder.encodeBool(isDeclined, forKey: "isDeclined")
+ case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired):
+ encoder.encodeInt32(57, forKey: "_rawValue")
+ encoder.encodeObject(gift, forKey: "gift")
+ encoder.encodeCodable(amount, forKey: "amount")
+ encoder.encodeBool(hasExpired, forKey: "hasExpired")
}
}
@@ -926,7 +950,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return [peerId]
case let .prizeStars(_, _, boostPeerId, _, _):
return boostPeerId.flatMap { [$0] } ?? []
- case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, toPeerId):
+ case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, toPeerId, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)
@@ -941,7 +965,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(toPeerId)
}
return peerIds
- case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _):
+ case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift
index 7910601f9d..bef68016e0 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift
@@ -210,52 +210,28 @@ public final class TelegramMediaWebpageGiftAuctionAttribute: PostboxCoding, Equa
if lhs.endDate != rhs.endDate {
return false
}
- if lhs.centerColor != rhs.centerColor {
- return false
- }
- if lhs.edgeColor != rhs.edgeColor {
- return false
- }
- if lhs.textColor != rhs.textColor {
- return false
- }
return true
}
public let gift: StarGift
public let endDate: Int32
- public let centerColor: Int32
- public let edgeColor: Int32
- public let textColor: Int32
-
+
public init(
gift: StarGift,
- endDate: Int32,
- centerColor: Int32,
- edgeColor: Int32,
- textColor: Int32
+ endDate: Int32
) {
self.gift = gift
self.endDate = endDate
- self.centerColor = centerColor
- self.edgeColor = edgeColor
- self.textColor = textColor
}
public init(decoder: PostboxDecoder) {
self.gift = decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift
self.endDate = decoder.decodeInt32ForKey("endDate", orElse: 0)
- self.centerColor = decoder.decodeInt32ForKey("centerColor", orElse: 0)
- self.edgeColor = decoder.decodeInt32ForKey("edgeColor", orElse: 0)
- self.textColor = decoder.decodeInt32ForKey("textColor", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObject(self.gift, forKey: "gift")
encoder.encodeInt32(self.endDate, forKey: "endDate")
- encoder.encodeInt32(self.centerColor, forKey: "centerColor")
- encoder.encodeInt32(self.edgeColor, forKey: "edgeColor")
- encoder.encodeInt32(self.textColor, forKey: "textColor")
}
}
diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift
index 5239cae769..37333a5dc7 100644
--- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift
+++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift
@@ -25,10 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
public let entities: [MessageTextEntity]
public let toLang: String
- public let additional:[Additional]
+ public let additional: [Additional]
public let pollSolution: Additional?
-
public var associatedPeerIds: [PeerId] {
return []
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift
index 4a8493f994..b35faecebb 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift
@@ -56,6 +56,10 @@ public extension TelegramEngineUnauthorized {
return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)
}
+ public func requestPasskeyLoginData(apiId: Int32, apiHash: String) -> Signal {
+ return _internal_requestPasskeyLoginData(network: self.account.network, apiId: apiId, apiHash: apiHash)
+ }
+
public func state() -> Signal {
return self.account.postbox.stateView()
|> map { view -> TelegramEngineAuthorizationState? in
@@ -210,6 +214,22 @@ public extension TelegramEngine {
public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal {
return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)
}
+
+ public func passkeysData() -> Signal<[TelegramPasskey], NoError> {
+ return _internal_passkeysData(network: self.account.network)
+ }
+
+ public func requestPasskeyRegistration() -> Signal {
+ return _internal_requestPasskeyRegistration(network: self.account.network)
+ }
+
+ public func requestCreatePasskey(id: String, clientData: String, attestationObject: Data) -> Signal {
+ return _internal_requestCreatePasskey(network: self.account.network, id: id, clientData: clientData, attestationObject: attestationObject)
+ }
+
+ public func deletePasskey(id: String) -> Signal {
+ return _internal_deletePasskey(network: self.account.network, id: id)
+ }
}
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift
index 33bcac50a0..c9f3d5ec2f 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessagesInteractively.swift
@@ -209,7 +209,7 @@ func _internal_clearHistoryInteractively(postbox: Postbox, peerId: PeerId, threa
func _internal_clearAuthorHistory(account: Account, peerId: PeerId, memberId: PeerId) -> Signal {
return account.postbox.transaction { transaction -> Signal in
if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputChannel = apiInputChannel(peer), let inputUser = apiInputPeer(memberPeer) {
- let signal = account.network.request(Api.functions.channels.deleteParticipantHistory(channel: inputChannel, participant: inputUser))
+ let signal = peer.isMonoForum ? .fail(true) : account.network.request(Api.functions.channels.deleteParticipantHistory(channel: inputChannel, participant: inputUser))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
index 584f8775a8..95e6913650 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift
@@ -14,7 +14,7 @@ func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to p
flags |= (1 << 13)
}
- return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil))
+ return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal in
return .single(nil)
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
index 2bad40a011..9133c0a5ac 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
@@ -593,7 +593,7 @@ public extension TelegramEngine {
}
public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
- return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang)
+ return _internal_translateTexts(network: self.account.network, texts: texts, toLang: toLang)
}
public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal {
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
index aaaaed5e05..28574d329d 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift
@@ -48,7 +48,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie
}
}
-func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
+func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> {
var flags: Int32 = 0
flags |= (1 << 1)
@@ -112,9 +112,9 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
return .never()
}
- let polls = messages.compactMap { msg in
- if let poll = msg.media.first as? TelegramMediaPoll {
- return (poll, msg.id)
+ let polls = messages.compactMap { message in
+ if let poll = message.media.first as? TelegramMediaPoll {
+ return (poll, message.id)
} else {
return nil
}
@@ -128,9 +128,19 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
if let solution = poll.results.solution {
texts.append((solution.text, solution.entities))
}
- return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang)
+ return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang)
}
+ let audioTranscriptions = messages.compactMap { message in
+ if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending {
+ return (audioTranscription.text, message.id)
+ } else {
+ return nil
+ }
+ }
+ let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in
+ return _internal_translate(network: account.network, text: text, toLang: toLang)
+ }
var flags: Int32 = 0
flags |= (1 << 0)
@@ -197,8 +207,8 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
}
}
- return combineLatest(msgs, combineLatest(pollSignals))
- |> mapToSignal { (result, pollResults) -> Signal in
+ return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals))
+ |> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal in
return account.postbox.transaction { transaction in
if case let .translateResult(results) = result {
var index = 0
@@ -218,6 +228,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
index += 1
}
}
+
if !pollResults.isEmpty {
for (i, poll) in polls.enumerated() {
let result = pollResults[i]
@@ -231,12 +242,12 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
if translated.0.isEmpty {
translated = (poll.0.options[i].text, poll.0.options[i].entities)
}
- attrOptions.append(.init(text: translated.0, entities: translated.1))
+ attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1))
}
let solution: TranslationMessageAttribute.Additional?
if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty {
- solution = .init(text: result[result.count - 1].0, entities: result[result.count - 1].1)
+ solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1)
} else {
solution = nil
}
@@ -251,6 +262,22 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin
}
}
}
+
+ if !audioTranscriptionsResults.isEmpty {
+ for (i, audioTranscription) in audioTranscriptions.enumerated() {
+ if let result = audioTranscriptionsResults[i] {
+ transaction.updateMessage(audioTranscription.1, update: { currentMessage in
+ let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
+ var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
+
+ let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang)
+ attributes.append(updatedAttribute)
+
+ return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
+ })
+ }
+ }
+ }
}
|> castError(TranslationError.self)
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
index 66b92fb194..266529cfd5 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift
@@ -62,6 +62,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case lockedUntilDate
case auctionSlug
case auctionGiftsPerRound
+ case auctionStartDate
+ case upgradeVariantsCount
+ case background
}
public struct Availability: Equatable, Codable, PostboxCoding {
@@ -169,6 +172,40 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
}
}
+ public struct Background: Equatable, Codable, PostboxCoding {
+ enum CodingKeys: String, CodingKey {
+ case centerColor
+ case edgeColor
+ case textColor
+ }
+
+ public let centerColor: Int32
+ public let edgeColor: Int32
+ public let textColor: Int32
+
+ public init(
+ centerColor: Int32,
+ edgeColor: Int32,
+ textColor: Int32
+ ) {
+ self.centerColor = centerColor
+ self.edgeColor = edgeColor
+ self.textColor = textColor
+ }
+
+ public init(decoder: PostboxDecoder) {
+ self.centerColor = decoder.decodeInt32ForKey(CodingKeys.centerColor.rawValue, orElse: 0)
+ self.edgeColor = decoder.decodeInt32ForKey(CodingKeys.edgeColor.rawValue, orElse: 0)
+ self.textColor = decoder.decodeInt32ForKey(CodingKeys.textColor.rawValue, orElse: 0)
+ }
+
+ public func encode(_ encoder: PostboxEncoder) {
+ encoder.encodeInt32(self.centerColor, forKey: CodingKeys.centerColor.rawValue)
+ encoder.encodeInt32(self.edgeColor, forKey: CodingKeys.edgeColor.rawValue)
+ encoder.encodeInt32(self.textColor, forKey: CodingKeys.textColor.rawValue)
+ }
+ }
+
public enum DecodingError: Error {
case generic
}
@@ -187,8 +224,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let lockedUntilDate: Int32?
public let auctionSlug: String?
public let auctionGiftsPerRound: Int32?
+ public let auctionStartDate: Int32?
+ public let upgradeVariantsCount: Int32?
+ public let background: Background?
- public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?, lockedUntilDate: Int32?, auctionSlug: String?, auctionGiftsPerRound: Int32?) {
+ public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?, lockedUntilDate: Int32?, auctionSlug: String?, auctionGiftsPerRound: Int32?, auctionStartDate: Int32?, upgradeVariantsCount: Int32?, background: Background?) {
self.id = id
self.title = title
self.file = file
@@ -203,6 +243,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.lockedUntilDate = lockedUntilDate
self.auctionSlug = auctionSlug
self.auctionGiftsPerRound = auctionGiftsPerRound
+ self.auctionStartDate = auctionStartDate
+ self.upgradeVariantsCount = upgradeVariantsCount
+ self.background = background
}
public init(from decoder: Decoder) throws {
@@ -227,6 +270,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.lockedUntilDate = try container.decodeIfPresent(Int32.self, forKey: .lockedUntilDate)
self.auctionSlug = try container.decodeIfPresent(String.self, forKey: .auctionSlug)
self.auctionGiftsPerRound = try container.decodeIfPresent(Int32.self, forKey: .auctionGiftsPerRound)
+ self.auctionStartDate = try container.decodeIfPresent(Int32.self, forKey: .auctionStartDate)
+ self.upgradeVariantsCount = try container.decodeIfPresent(Int32.self, forKey: .upgradeVariantsCount)
+ self.background = try container.decodeIfPresent(Background.self, forKey: .background)
}
public init(decoder: PostboxDecoder) {
@@ -244,6 +290,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.lockedUntilDate = decoder.decodeOptionalInt32ForKey(CodingKeys.lockedUntilDate.rawValue)
self.auctionSlug = decoder.decodeOptionalStringForKey(CodingKeys.auctionSlug.rawValue)
self.auctionGiftsPerRound = decoder.decodeOptionalInt32ForKey(CodingKeys.auctionGiftsPerRound.rawValue)
+ self.auctionStartDate = decoder.decodeOptionalInt32ForKey(CodingKeys.auctionStartDate.rawValue)
+ self.upgradeVariantsCount = decoder.decodeOptionalInt32ForKey(CodingKeys.upgradeVariantsCount.rawValue)
+ self.background = decoder.decodeObjectForKey(CodingKeys.background.rawValue, decoder: { StarGift.Gift.Background(decoder: $0) }) as? StarGift.Gift.Background
}
public func encode(to encoder: Encoder) throws {
@@ -267,6 +316,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encodeIfPresent(self.lockedUntilDate, forKey: .lockedUntilDate)
try container.encodeIfPresent(self.auctionSlug, forKey: .auctionSlug)
try container.encodeIfPresent(self.auctionGiftsPerRound, forKey: .auctionGiftsPerRound)
+ try container.encodeIfPresent(self.auctionStartDate, forKey: .auctionStartDate)
+ try container.encodeIfPresent(self.upgradeVariantsCount, forKey: .upgradeVariantsCount)
+ try container.encodeIfPresent(self.background, forKey: .background)
}
public func encode(_ encoder: PostboxEncoder) {
@@ -320,6 +372,21 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
} else {
encoder.encodeNil(forKey: CodingKeys.auctionGiftsPerRound.rawValue)
}
+ if let auctionStartDate = self.auctionStartDate {
+ encoder.encodeInt32(auctionStartDate, forKey: CodingKeys.auctionStartDate.rawValue)
+ } else {
+ encoder.encodeNil(forKey: CodingKeys.auctionStartDate.rawValue)
+ }
+ if let upgradeVariantsCount = self.upgradeVariantsCount {
+ encoder.encodeInt32(upgradeVariantsCount, forKey: CodingKeys.upgradeVariantsCount.rawValue)
+ } else {
+ encoder.encodeNil(forKey: CodingKeys.upgradeVariantsCount.rawValue)
+ }
+ if let background = self.background {
+ encoder.encodeObject(background, forKey: CodingKeys.background.rawValue)
+ } else {
+ encoder.encodeNil(forKey: CodingKeys.background.rawValue)
+ }
}
}
@@ -342,10 +409,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
case releasedBy
case valueAmount
case valueCurrency
+ case valueUsdAmount
case flags
case themePeerId
case peerColor
case hostPeerId
+ case minOfferStars
}
public struct Flags: OptionSet {
@@ -631,12 +700,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
public let releasedBy: EnginePeer.Id?
public let valueAmount: Int64?
public let valueCurrency: String?
+ public let valueUsdAmount: Int64?
public let flags: Flags
public let themePeerId: EnginePeer.Id?
public let peerColor: PeerCollectibleColor?
public let hostPeerId: EnginePeer.Id?
+ public let minOfferStars: Int64?
- public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?) {
+ public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, valueUsdAmount: Int64?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?, minOfferStars: Int64?) {
self.id = id
self.giftId = giftId
self.title = title
@@ -651,10 +722,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = releasedBy
self.valueAmount = valueAmount
self.valueCurrency = valueCurrency
+ self.valueUsdAmount = valueUsdAmount
self.flags = flags
self.themePeerId = themePeerId
self.peerColor = peerColor
self.hostPeerId = hostPeerId
+ self.minOfferStars = minOfferStars
}
public init(from decoder: Decoder) throws {
@@ -687,10 +760,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy)
self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount)
self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency)
+ self.valueUsdAmount = try container.decodeIfPresent(Int64.self, forKey: .valueUsdAmount)
self.flags = try container.decodeIfPresent(Int32.self, forKey: .flags).flatMap { Flags(rawValue: $0) } ?? []
self.themePeerId = try container.decodeIfPresent(Int64.self, forKey: .themePeerId).flatMap { EnginePeer.Id($0) }
self.peerColor = try container.decodeIfPresent(PeerCollectibleColor.self, forKey: .peerColor)
self.hostPeerId = try container.decodeIfPresent(Int64.self, forKey: .hostPeerId).flatMap { EnginePeer.Id($0) }
+ self.minOfferStars = try container.decodeIfPresent(Int64.self, forKey: .minOfferStars)
}
public init(decoder: PostboxDecoder) {
@@ -722,10 +797,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) }
self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue)
self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue)
+ self.valueUsdAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueUsdAmount.rawValue)
self.flags = decoder.decodeOptionalInt32ForKey(CodingKeys.flags.rawValue).flatMap { Flags(rawValue: $0) } ?? []
self.themePeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.themePeerId.rawValue).flatMap { EnginePeer.Id($0) }
self.peerColor = decoder.decodeCodable(PeerCollectibleColor.self, forKey: CodingKeys.peerColor.rawValue)
self.hostPeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.hostPeerId.rawValue).flatMap { EnginePeer.Id($0) }
+ self.minOfferStars = decoder.decodeOptionalInt64ForKey(CodingKeys.minOfferStars.rawValue)
}
public func encode(to encoder: Encoder) throws {
@@ -751,10 +828,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy)
try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount)
try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency)
+ try container.encodeIfPresent(self.valueUsdAmount, forKey: .valueUsdAmount)
try container.encode(self.flags.rawValue, forKey: .flags)
try container.encodeIfPresent(self.themePeerId?.toInt64(), forKey: .themePeerId)
try container.encodeIfPresent(self.peerColor, forKey: .peerColor)
try container.encodeIfPresent(self.hostPeerId?.toInt64(), forKey: .hostPeerId)
+ try container.encodeIfPresent(self.minOfferStars, forKey: .minOfferStars)
}
public func encode(_ encoder: PostboxEncoder) {
@@ -796,6 +875,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue)
encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue)
}
+ if let valueUsdAmount = self.valueUsdAmount {
+ encoder.encodeInt64(valueUsdAmount, forKey: CodingKeys.valueUsdAmount.rawValue)
+ } else {
+ encoder.encodeNil(forKey: CodingKeys.valueUsdAmount.rawValue)
+ }
encoder.encodeInt32(self.flags.rawValue, forKey: CodingKeys.flags.rawValue)
if let themePeerId = self.themePeerId {
encoder.encodeInt64(themePeerId.toInt64(), forKey: CodingKeys.themePeerId.rawValue)
@@ -812,6 +896,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
} else {
encoder.encodeNil(forKey: CodingKeys.hostPeerId.rawValue)
}
+ if let minOfferStars = self.minOfferStars {
+ encoder.encodeInt64(minOfferStars, forKey: CodingKeys.minOfferStars.rawValue)
+ } else {
+ encoder.encodeNil(forKey: CodingKeys.minOfferStars.rawValue)
+ }
}
public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift {
@@ -830,10 +919,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
+ valueUsdAmount: self.valueUsdAmount,
flags: self.flags,
themePeerId: self.themePeerId,
peerColor: self.peerColor,
- hostPeerId: self.hostPeerId
+ hostPeerId: self.hostPeerId,
+ minOfferStars: self.minOfferStars
)
}
@@ -853,10 +944,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
+ valueUsdAmount: self.valueUsdAmount,
flags: self.flags,
themePeerId: self.themePeerId,
peerColor: self.peerColor,
- hostPeerId: self.hostPeerId
+ hostPeerId: self.hostPeerId,
+ minOfferStars: self.minOfferStars
)
}
@@ -876,10 +969,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
+ valueUsdAmount: self.valueUsdAmount,
flags: self.flags,
themePeerId: themePeerId,
peerColor: self.peerColor,
- hostPeerId: self.hostPeerId
+ hostPeerId: self.hostPeerId,
+ minOfferStars: self.minOfferStars
)
}
@@ -899,10 +994,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
+ valueUsdAmount: self.valueUsdAmount,
flags: self.flags,
themePeerId: self.themePeerId,
peerColor: self.peerColor,
- hostPeerId: self.hostPeerId
+ hostPeerId: self.hostPeerId,
+ minOfferStars: self.minOfferStars
)
}
}
@@ -978,6 +1075,15 @@ public extension StarGift {
}
}
+ var title: String {
+ switch self {
+ case let .generic(gift):
+ return gift.title ?? ""
+ case let .unique(gift):
+ return gift.title
+ }
+ }
+
var releasedBy: EnginePeer.Id? {
switch self {
case let .generic(gift):
@@ -991,7 +1097,7 @@ public extension StarGift {
extension StarGift {
init?(apiStarGift: Api.StarGift) {
switch apiStarGift {
- case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains, lockedUntilDate, auctionSlug, giftsPerRound):
+ case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains, lockedUntilDate, auctionSlug, giftsPerRound, auctionStartDate, upgradeVariantsCount, apiBackground):
var flags = StarGift.Gift.Flags()
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.isBirthdayGift)
@@ -1023,11 +1129,36 @@ extension StarGift {
if let perUserTotal, let perUserRemains {
perUserLimit = StarGift.Gift.PerUserLimit(total: perUserTotal, remains: perUserRemains)
}
+ var background: StarGift.Gift.Background?
+ switch apiBackground {
+ case let .starGiftBackground(centerColor, edgeColor, textColor):
+ background = StarGift.Gift.Background(centerColor: centerColor, edgeColor: edgeColor, textColor: textColor)
+ default:
+ break
+ }
guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else {
return nil
}
- self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate, auctionSlug: auctionSlug, auctionGiftsPerRound: giftsPerRound))
- case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer, peerColor, hostPeerId):
+ self = .generic(StarGift.Gift(
+ id: id,
+ title: title,
+ file: file,
+ price: stars,
+ convertStars: convertStars,
+ availability: availability,
+ soldOut: soldOut,
+ flags: flags,
+ upgradeStars: upgradeStars,
+ releasedBy: releasedBy?.peerId,
+ perUserLimit: perUserLimit,
+ lockedUntilDate: lockedUntilDate,
+ auctionSlug: auctionSlug,
+ auctionGiftsPerRound: giftsPerRound,
+ auctionStartDate: auctionStartDate,
+ upgradeVariantsCount: upgradeVariantsCount,
+ background: background
+ ))
+ case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, valueUsdAmount, themePeer, peerColor, hostPeerId, minOfferStars):
let owner: StarGift.UniqueGift.Owner
if let ownerAddress {
owner = .address(ownerAddress)
@@ -1059,7 +1190,29 @@ extension StarGift {
break
}
- self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId, peerColor: peerCollectibleColor, hostPeerId: hostPeerId?.peerId))
+ self = .unique(StarGift.UniqueGift(
+ id: id,
+ giftId: giftId,
+ title: title,
+ number: num,
+ slug: slug,
+ owner: owner,
+ attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0)
+ },
+ availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal),
+ giftAddress: giftAddress,
+ resellAmounts: resellAmounts,
+ resellForTonOnly: (apiFlags & (1 << 7)) != 0,
+ releasedBy: releasedBy?.peerId,
+ valueAmount: valueAmount,
+ valueCurrency: valueCurrency,
+ valueUsdAmount: valueUsdAmount,
+ flags: flags,
+ themePeerId: themePeer?.peerId,
+ peerColor: peerCollectibleColor,
+ hostPeerId: hostPeerId?.peerId,
+ minOfferStars: minOfferStars.flatMap { Int64($0) }
+ ))
}
}
}
@@ -1270,7 +1423,7 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var media = currentMessage.media
- if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned) = action.action, case let .unique(uniqueGift) = gift {
+ if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
media = [
TelegramMediaAction(
@@ -1290,7 +1443,9 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift
canTransferDate: canTransferDate,
canResaleDate: canResaleDate,
dropOriginalDetailsStars: nil,
- assigned: assigned)
+ assigned: assigned,
+ fromOffer: fromOffer
+ )
)
]
}
@@ -1395,7 +1550,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
case let .updateNewMessage(message, _, _):
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
- if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _) = action.action, case let .Id(messageId) = message.id {
+ if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@@ -1422,7 +1577,8 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
collectionIds: nil,
prepaidUpgradeHash: nil,
upgradeSeparate: false,
- dropOriginalDetailsStars: dropOriginalDetailsStars
+ dropOriginalDetailsStars: dropOriginalDetailsStars,
+ number: nil
))
}
}
@@ -2376,6 +2532,7 @@ public final class ProfileGiftsContext {
case prepaidUpgradeHash
case upgradeSeparate
case dropOriginalDetailsStars
+ case number
}
public let gift: TelegramCore.StarGift
@@ -2398,6 +2555,7 @@ public final class ProfileGiftsContext {
public let prepaidUpgradeHash: String?
public let upgradeSeparate: Bool
public let dropOriginalDetailsStars: Int64?
+ public let number: Int32?
fileprivate let _fromPeerId: EnginePeer.Id?
@@ -2425,7 +2583,8 @@ public final class ProfileGiftsContext {
collectionIds: [Int32]?,
prepaidUpgradeHash: String?,
upgradeSeparate: Bool,
- dropOriginalDetailsStars: Int64?
+ dropOriginalDetailsStars: Int64?,
+ number: Int32?
) {
self.gift = gift
self.reference = reference
@@ -2448,6 +2607,7 @@ public final class ProfileGiftsContext {
self.prepaidUpgradeHash = prepaidUpgradeHash
self.upgradeSeparate = upgradeSeparate
self.dropOriginalDetailsStars = dropOriginalDetailsStars
+ self.number = number
}
public init(from decoder: Decoder) throws {
@@ -2480,6 +2640,7 @@ public final class ProfileGiftsContext {
self.prepaidUpgradeHash = try container.decodeIfPresent(String.self, forKey: .prepaidUpgradeHash)
self.upgradeSeparate = try container.decodeIfPresent(Bool.self, forKey: .upgradeSeparate) ?? false
self.dropOriginalDetailsStars = try container.decodeIfPresent(Int64.self, forKey: .dropOriginalDetailsStars)
+ self.number = try container.decodeIfPresent(Int32.self, forKey: .number)
}
public func encode(to encoder: Encoder) throws {
@@ -2505,6 +2666,7 @@ public final class ProfileGiftsContext {
try container.encodeIfPresent(self.prepaidUpgradeHash, forKey: .prepaidUpgradeHash)
try container.encode(self.upgradeSeparate, forKey: .upgradeSeparate)
try container.encodeIfPresent(self.dropOriginalDetailsStars, forKey: .dropOriginalDetailsStars)
+ try container.encodeIfPresent(self.number, forKey: .number)
}
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
@@ -2528,7 +2690,8 @@ public final class ProfileGiftsContext {
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate,
- dropOriginalDetailsStars: self.dropOriginalDetailsStars
+ dropOriginalDetailsStars: self.dropOriginalDetailsStars,
+ number: self.number
)
}
@@ -2553,7 +2716,8 @@ public final class ProfileGiftsContext {
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate,
- dropOriginalDetailsStars: self.dropOriginalDetailsStars
+ dropOriginalDetailsStars: self.dropOriginalDetailsStars,
+ number: self.number
)
}
@@ -2578,7 +2742,8 @@ public final class ProfileGiftsContext {
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate,
- dropOriginalDetailsStars: self.dropOriginalDetailsStars
+ dropOriginalDetailsStars: self.dropOriginalDetailsStars,
+ number: self.number
)
}
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
@@ -2602,7 +2767,8 @@ public final class ProfileGiftsContext {
collectionIds: self.collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate,
- dropOriginalDetailsStars: self.dropOriginalDetailsStars
+ dropOriginalDetailsStars: self.dropOriginalDetailsStars,
+ number: self.number
)
}
@@ -2627,7 +2793,8 @@ public final class ProfileGiftsContext {
collectionIds: collectionIds,
prepaidUpgradeHash: self.prepaidUpgradeHash,
upgradeSeparate: self.upgradeSeparate,
- dropOriginalDetailsStars: self.dropOriginalDetailsStars
+ dropOriginalDetailsStars: self.dropOriginalDetailsStars,
+ number: self.number
)
}
}
@@ -2845,7 +3012,7 @@ public final class ProfileGiftsContext {
extension ProfileGiftsContext.State.StarGift {
init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) {
switch apiSavedStarGift {
- case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, dropOriginalDetailsStars):
+ case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, dropOriginalDetailsStars, number):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
@@ -2895,6 +3062,7 @@ extension ProfileGiftsContext.State.StarGift {
self.prepaidUpgradeHash = prepaidUpgradeHash
self.upgradeSeparate = (flags & (1 << 17)) != 0
self.dropOriginalDetailsStars = dropOriginalDetailsStars
+ self.number = number
}
}
}
@@ -2962,8 +3130,6 @@ func _internal_getUniqueStarGiftValueInfo(account: Account, slug: String) -> Sig
if let result {
switch result {
case let .uniqueStarGiftValueInfo(flags, currency, value, initialSaleDate, initialSaleStars, initialSalePrice, lastSaleDate, lastSalePrice, floorPrice, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
- let _ = listedCount
- let _ = fragmentListedCount
return StarGift.UniqueGift.ValueInfo(
isLastSaleOnFragment: flags & (1 << 1) != 0,
valueIsAverage: flags & (1 << 6) != 0,
@@ -3623,3 +3789,65 @@ public final class ResaleGiftsContext {
return state
}
}
+
+func giftUpgradesId(giftId: Int64) -> ItemCacheEntryId {
+ let cacheKey = ValueBoxKey(length: 8)
+ cacheKey.setInt64(0, value: giftId)
+ return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGiftUpgradesAttributes, key: cacheKey)
+}
+
+final class CachedStartGiftUpgradeAttributes: Codable {
+ enum CodingKeys: String, CodingKey {
+ case attributes
+ }
+
+ var attributes: [StarGift.UniqueGift.Attribute]
+
+ init(attributes: [StarGift.UniqueGift.Attribute]) {
+ self.attributes = attributes
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ self.attributes = try container.decode([StarGift.UniqueGift.Attribute].self, forKey: .attributes)
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(self.attributes, forKey: .attributes)
+ }
+}
+
+func _internal_getStarGiftUpgradeAttributes(account: Account, giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> {
+ return account.postbox.transaction { transaction in
+ if let cachedGifts = transaction.retrieveItemCacheEntry(id: giftUpgradesId(giftId: giftId))?.get(CachedStartGiftUpgradeAttributes.self) {
+ return .single(cachedGifts.attributes)
+ } else {
+ return account.network.request(Api.functions.payments.getStarGiftUpgradeAttributes(giftId: giftId))
+ |> map(Optional.init)
+ |> `catch` { _ -> Signal in
+ return .single(nil)
+ }
+ |> mapToSignal { result -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> in
+ guard let result else {
+ return .single(nil)
+ }
+ switch result {
+ case let .starGiftUpgradeAttributes(apiAttributes):
+ let attributes = apiAttributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) }
+ return account.postbox.transaction { transaction in
+ if !attributes.isEmpty {
+ if let entry = CodableEntry(CachedStartGiftUpgradeAttributes(attributes: attributes)) {
+ transaction.putItemCacheEntry(id: giftUpgradesId(giftId: giftId), entry: entry)
+ }
+ }
+ return attributes
+ }
+ }
+ }
+ }
+ }
+ |> switchToLatest
+}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift
index f213cc72b7..10ed47ea5a 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift
@@ -30,8 +30,8 @@ private func _internal_getStarGiftAuctionState(postbox: Postbox, network: Networ
}
return postbox.transaction { transaction -> (gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)? in
switch result {
- case let .starGiftAuctionState(apiGift, state, userState, timeout, users):
- updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
+ case let .starGiftAuctionState(apiGift, state, userState, timeout, users, chats):
+ updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(chats: chats, users: users))
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
@@ -54,9 +54,28 @@ public final class GiftAuctionContext {
public var date: Int32
}
+ public enum Round: Equatable {
+ case generic(num: Int32, duration: Int32)
+ case extendable(num: Int32, duration: Int32, extendTop: Int32, extendWindow: Int32)
+
+ public var num: Int32 {
+ switch self {
+ case let .generic(num, _), let .extendable(num, _, _, _):
+ return num
+ }
+ }
+
+ public var duration: Int32 {
+ switch self {
+ case let .generic(_, duration), let .extendable(_, duration, _, _):
+ return duration
+ }
+ }
+ }
+
public enum AuctionState: Equatable {
- case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32)
- case finished(startDate: Int32, endDate: Int32, averagePrice: Int64)
+ case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32, rounds: [Round], lastGiftNumber: Int32)
+ case finished(startDate: Int32, endDate: Int32, averagePrice: Int64, listedCount: Int32?, fragmentListedCount: Int32?, fragmentListedUrl: String?)
}
public struct MyState: Equatable {
@@ -114,6 +133,15 @@ public final class GiftAuctionContext {
}
}
+ public var isUpcoming: Bool {
+ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
+ if case let .ongoing(_, startTime, _, _, _, _, _, _, _, _, _, _) = self.auctionState {
+ return currentTime < startTime
+ } else {
+ return false
+ }
+ }
+
public convenience init(account: Account, gift: StarGift) {
self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil, initialTimeout: nil)
}
@@ -136,7 +164,7 @@ public final class GiftAuctionContext {
private var currentVersion: Int32 {
var currentVersion: Int32 = 0
- if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = self.auctionState {
+ if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = self.auctionState {
currentVersion = version
}
return currentVersion
@@ -154,7 +182,7 @@ public final class GiftAuctionContext {
return
}
- if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
+ if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
} else if let auctionState {
self.auctionState = auctionState
}
@@ -163,7 +191,7 @@ public final class GiftAuctionContext {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var effectiveTimeout = timeout
- if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState {
+ if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState {
let delta = nextRoundDate - currentTime
if delta > 0 && delta < timeout {
effectiveTimeout = delta
@@ -184,7 +212,7 @@ public final class GiftAuctionContext {
}
func updateAuctionState(_ auctionState: GiftAuctionContext.State.AuctionState) {
- if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
+ if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion {
} else {
self.auctionState = auctionState
}
@@ -225,16 +253,45 @@ extension GiftAuctionContext.State.BidLevel {
extension GiftAuctionContext.State.AuctionState {
init?(apiAuctionState: Api.StarGiftAuctionState, peers: [PeerId: Peer]) {
switch apiAuctionState {
- case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, giftsLeft, currentRound, totalRounds):
+ case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds):
var topBidders: [EnginePeer] = []
for peerId in topBiddersPeerIds {
if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))] {
topBidders.append(EnginePeer(peer))
}
}
- self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds)
- case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice):
- self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice)
+ var rounds: [GiftAuctionContext.State.Round] = []
+ for apiRound in apiRounds {
+ switch apiRound {
+ case let .starGiftAuctionRound(num, duration):
+ rounds.append(.generic(num: num, duration: duration))
+ case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow):
+ rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow))
+ }
+ }
+ self = .ongoing(
+ version: version,
+ startDate: startDate,
+ endDate: endDate,
+ minBidAmount: minBidAmount,
+ bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)),
+ topBidders: topBidders,
+ nextRoundDate: nextRoundAt,
+ giftsLeft: giftsLeft,
+ currentRound: currentRound,
+ totalRounds: totalRounds,
+ rounds: rounds,
+ lastGiftNumber: lastGiftNumber
+ )
+ case let .starGiftAuctionStateFinished(_, startDate, endDate, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
+ self = .finished(
+ startDate: startDate,
+ endDate: endDate,
+ averagePrice: averagePrice,
+ listedCount: listedCount,
+ fragmentListedCount: fragmentListedCount,
+ fragmentListedUrl: fragmentListedUrl
+ )
case .starGiftAuctionStateNotModified:
return nil
}
@@ -242,16 +299,45 @@ extension GiftAuctionContext.State.AuctionState {
init?(apiAuctionState: Api.StarGiftAuctionState, transaction: Transaction) {
switch apiAuctionState {
- case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, giftsLeft, currentRound, totalRounds):
+ case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds):
var topBidders: [EnginePeer] = []
for peerId in topBiddersPeerIds {
if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))) {
topBidders.append(EnginePeer(peer))
}
}
- self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds)
- case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice):
- self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice)
+ var rounds: [GiftAuctionContext.State.Round] = []
+ for apiRound in apiRounds {
+ switch apiRound {
+ case let .starGiftAuctionRound(num, duration):
+ rounds.append(.generic(num: num, duration: duration))
+ case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow):
+ rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow))
+ }
+ }
+ self = .ongoing(
+ version: version,
+ startDate: startDate,
+ endDate: endDate,
+ minBidAmount: minBidAmount,
+ bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)),
+ topBidders: topBidders,
+ nextRoundDate: nextRoundAt,
+ giftsLeft: giftsLeft,
+ currentRound: currentRound,
+ totalRounds: totalRounds,
+ rounds: rounds,
+ lastGiftNumber: lastGiftNumber
+ )
+ case let .starGiftAuctionStateFinished(_, startDate, endDate, averagePrice, listedCount, fragmentListedCount, fragmentListedUrl):
+ self = .finished(
+ startDate: startDate,
+ endDate: endDate,
+ averagePrice: averagePrice,
+ listedCount: listedCount,
+ fragmentListedCount: fragmentListedCount,
+ fragmentListedUrl: fragmentListedUrl
+ )
case .starGiftAuctionStateNotModified:
return nil
}
@@ -281,6 +367,7 @@ public struct GiftAuctionAcquiredGift: Equatable {
public let position: Int32
public let text: String?
public let entities: [MessageTextEntity]?
+ public let number: Int32?
}
func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> Signal<[GiftAuctionAcquiredGift], NoError> {
@@ -302,7 +389,7 @@ func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> S
var mappedGifts: [GiftAuctionAcquiredGift] = []
for gift in gifts {
switch gift {
- case let .starGiftAuctionAcquiredGift(flags, peerId, date, bidAmount, round, pos, message):
+ case let .starGiftAuctionAcquiredGift(flags, peerId, date, bidAmount, round, pos, message, number):
if let peer = transaction.getPeer(peerId.peerId) {
var text: String?
var entities: [MessageTextEntity]?
@@ -321,7 +408,8 @@ func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> S
round: round,
position: pos,
text: text,
- entities: entities
+ entities: entities,
+ number: number
))
}
}
@@ -338,8 +426,8 @@ func _internal_getActiveGiftAuctions(account: Account, hash: Int64) -> Signal<[G
|> mapToSignal { result in
return account.postbox.transaction { transaction -> [GiftAuctionContext]? in
switch result {
- case let .starGiftActiveAuctions(auctions, users):
- let parsedPeers = AccumulatedPeers(users: users)
+ case let .starGiftActiveAuctions(auctions, users, chats):
+ let parsedPeers = AccumulatedPeers(chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers)
var auctionContexts: [GiftAuctionContext] = []
@@ -493,7 +581,7 @@ public class GiftAuctionsManager {
public extension GiftAuctionContext.State {
func getPlace(myBid: Int64?, myBidDate: Int32?) -> Int32? {
- guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = self.auctionState else {
+ guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _, _, _) = self.auctionState else {
return nil
}
guard let myBid = myBid ?? self.myState.bidAmount else {
@@ -550,18 +638,18 @@ public extension GiftAuctionContext.State {
var startDate: Int32 {
switch self.auctionState {
- case let .ongoing(_, startDate, _, _, _, _, _, _, _, _):
+ case let .ongoing(_, startDate, _, _, _, _, _, _, _, _, _, _):
return startDate
- case let .finished(startDate, _, _):
+ case let .finished(startDate, _, _, _, _, _):
return startDate
}
}
var endDate: Int32 {
switch self.auctionState {
- case let .ongoing(_, _, endDate, _, _, _, _, _, _, _):
+ case let .ongoing(_, _, endDate, _, _, _, _, _, _, _, _, _):
return endDate
- case let .finished(_, endDate, _):
+ case let .finished(_, endDate, _, _, _, _):
return endDate
}
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift
new file mode 100644
index 0000000000..acb147757a
--- /dev/null
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift
@@ -0,0 +1,55 @@
+import Foundation
+import Postbox
+import MtProtoKit
+import SwiftSignalKit
+import TelegramApi
+
+public enum ResolveStarGiftOfferError {
+ case generic
+}
+
+func _internal_resolveStarGiftOffer(account: Account, messageId: EngineMessage.Id, accept: Bool) -> Signal {
+ var flags: Int32 = 0
+ if !accept {
+ flags |= (1 << 0)
+ }
+ return account.network.request(Api.functions.payments.resolveStarGiftOffer(flags: flags, offerMsgId: messageId.id))
+ |> mapError { _ -> ResolveStarGiftOfferError in
+ return .generic
+ }
+ |> mapToSignal { updates -> Signal in
+ account.stateManager.addUpdates(updates)
+ return .complete()
+ }
+ |> ignoreValues
+}
+
+
+public enum SendStarGiftOfferError {
+ case generic
+}
+
+func _internal_sendStarGiftOffer(account: Account, peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal {
+ var flags: Int32 = 0
+ if let _ = allowPaidStars {
+ flags |= (1 << 0)
+ }
+ return account.postbox.transaction { transaction in
+ return transaction.getPeer(peerId).flatMap(apiInputPeer)
+ }
+ |> castError(SendStarGiftOfferError.self)
+ |> mapToSignal { inputPeer -> Signal in
+ guard let inputPeer else {
+ return .fail(.generic)
+ }
+ return account.network.request(Api.functions.payments.sendStarGiftOffer(flags: flags, peer: inputPeer, slug: slug, price: amount.apiAmount, duration: duration, randomId: Int64.random(in: .min ..< .max), allowPaidStars: allowPaidStars))
+ |> mapError { _ -> SendStarGiftOfferError in
+ return .generic
+ }
+ |> mapToSignal { updates -> Signal in
+ account.stateManager.addUpdates(updates)
+ return .complete()
+ }
+ }
+ |> ignoreValues
+}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift
index 4bddd073cc..101466346d 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift
@@ -735,6 +735,9 @@ private extension StarsContext.State.Transaction {
if (apiFlags & (1 << 28)) != 0 {
flags.insert(.isStarGiftAuctionBid)
}
+ if (apiFlags & (1 << 29)) != 0 {
+ flags.insert(.isStarGiftOffer)
+ }
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
@@ -793,6 +796,7 @@ public final class StarsContext {
public static let isStarGiftDropOriginalDetails = Flags(rawValue: 1 << 12)
public static let isStarGiftAuctionBid = Flags(rawValue: 1 << 13)
public static let isLiveStreamPaidMessage = Flags(rawValue: 1 << 14)
+ public static let isStarGiftOffer = Flags(rawValue: 1 << 15)
}
public enum Peer: Equatable {
@@ -1640,7 +1644,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid:
receiptMessageId = nil
}
- } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _) = action.action, case let .Id(messageId) = message.id {
+ } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@@ -1667,7 +1671,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
collectionIds: nil,
prepaidUpgradeHash: nil,
upgradeSeparate: false,
- dropOriginalDetailsStars: dropOriginalDetailsStars
+ dropOriginalDetailsStars: dropOriginalDetailsStars,
+ number: nil
)
}
}
diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift
index f92f22385b..ec9b54d7c2 100644
--- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift
+++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift
@@ -180,6 +180,18 @@ public extension TelegramEngine {
public func getStarsTransaction(reference: StarsTransactionReference) -> Signal {
return _internal_getStarsTransaction(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, transactionReference: reference)
}
+
+ public func resolveStarGiftOffer(messageId: EngineMessage.Id, accept: Bool) -> Signal {
+ return _internal_resolveStarGiftOffer(account: self.account, messageId: messageId, accept: accept)
+ }
+
+ public func sendStarGiftOffer(peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal {
+ return _internal_sendStarGiftOffer(account: self.account, peerId: peerId, slug: slug, amount: amount, duration: duration, allowPaidStars: allowPaidStars)
+ }
+
+ public func getStarGiftUpgradeAttributes(giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> {
+ return _internal_getStarGiftUpgradeAttributes(account: self.account, giftId: giftId)
+ }
}
}
diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
index 3855648936..01c037b5c9 100644
--- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
+++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift
@@ -1171,7 +1171,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = mutableString
case .prizeStars:
attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor)
- case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate, isAuctionAcquired, _):
+ case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate, isAuctionAcquired, _, _):
if !forAdditionalServiceMessage {
if let text {
let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()))
@@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
- case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned):
+ case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
@@ -1290,7 +1290,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
}
} else {
- if message.id.peerId == accountPeerId && assigned {
+ if fromOffer, let resaleStars {
+ let starsString: String
+ switch resaleStars.currency {
+ case .stars:
+ starsString = strings.Notification_StarsGiftOffer_Accepted_Stars(Int32(clamping: resaleStars.amount.value))
+ case .ton:
+ starsString = formatTonAmountText(resaleStars.amount.value, dateTimeFormat: dateTimeFormat) + " TON"
+ }
+ let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
+ var peerName = ""
+ if let name = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle {
+ peerName = name
+ }
+ if message.author?.id == accountPeerId {
+ let peerIds: [(Int, EnginePeer.Id?)] = [(1, message.id.peerId)]
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[0] = boldAttributes
+ attributes[2] = boldAttributes
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_AcceptedYou(giftTitle, peerName, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ } else {
+ let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[1] = boldAttributes
+ attributes[2] = boldAttributes
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Accepted(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ }
+ } else if message.id.peerId == accountPeerId && assigned {
let attributes: [Int: MarkdownAttributeSet] = [0: boldAttributes]
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Assigned(giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
@@ -1616,6 +1642,103 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
+ case let .starGiftPurchaseOffer(gift, amount, _, _, _):
+ let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
+
+ let giftTitle: String
+ if case let .unique(gift) = gift {
+ giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
+ } else {
+ giftTitle = ""
+ }
+
+ let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)]
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[1] = boldAttributes
+ attributes[2] = boldAttributes
+
+ if message.author?.id == accountPeerId {
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Notification_StarsGiftOffer_OfferYou_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_OfferYou(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ } else {
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Notification_StarsGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Offer(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ }
+ case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired):
+ let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
+ let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)]
+
+ let giftTitle: String
+ if case let .unique(gift) = gift {
+ giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))"
+ } else {
+ giftTitle = ""
+ }
+
+ if hasExpired {
+ if message.author?.id == accountPeerId {
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Notification_StarsGiftOffer_ExpiredYou_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[1] = boldAttributes
+ attributes[2] = boldAttributes
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_ExpiredYou(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ } else {
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Notification_StarsGiftOffer_Expired_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ let timeString = "[TODO]"
+
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[1] = boldAttributes
+ attributes[2] = boldAttributes
+ attributes[3] = boldAttributes
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Expired(peerName, giftTitle, timeString, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ }
+ } else {
+ if message.author?.id == accountPeerId {
+ let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_RejectedYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ } else {
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Notification_StarsGiftOffer_Rejected_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
+ attributes[1] = boldAttributes
+ attributes[2] = boldAttributes
+ attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Rejected(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
+ }
+ }
case .unknown:
attributedString = nil
}
diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD
index 83a0f17408..8d395730a6 100644
--- a/submodules/TelegramUI/BUILD
+++ b/submodules/TelegramUI/BUILD
@@ -402,6 +402,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
+ "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode",
"//submodules/TelegramUI/Components/Chat/ChatRecentActionsController",
@@ -497,6 +498,8 @@ swift_library(
"//submodules/TelegramUI/Components/AttachmentFileController",
"//submodules/TelegramUI/Components/Contacts/NewContactScreen",
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
+ "//submodules/TelegramUI/Components/Settings/PasskeysScreen",
+ "//submodules/TelegramUI/Components/Gifts/GiftDemoScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],
diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift
index 69bab1e9b0..dc1f55617c 100644
--- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift
+++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift
@@ -108,6 +108,7 @@ final class AdminUserActionsPeerComponent: Component {
private weak var state: EmptyComponentState?
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift
index 07c787129a..59b5b88acf 100644
--- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift
+++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift
@@ -18,23 +18,29 @@ import ComponentFlow
import GlassBarButtonComponent
import BundleIconComponent
import EdgeEffect
+import SaveToCameraRoll
private final class AttachmentFileControllerArguments {
let context: AccountContext
+ let isAudio: Bool
let openGallery: () -> Void
let openFiles: () -> Void
+ let expandSavedMusic: () -> Void
let send: (Message) -> Void
- init(context: AccountContext, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, send: @escaping (Message) -> Void) {
+ init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
+ self.isAudio = isAudio
self.openGallery = openGallery
self.openFiles = openFiles
+ self.expandSavedMusic = expandSavedMusic
self.send = send
}
}
private enum AttachmentFileSection: Int32 {
case select
+ case savedMusic
case recent
}
@@ -55,6 +61,10 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromGallery(PresentationTheme, String)
case selectFromFiles(PresentationTheme, String)
+ case savedHeader(PresentationTheme, String)
+ case savedFile(Int32, PresentationTheme, Message?)
+ case showMore(PresentationTheme, String)
+
case recentHeader(PresentationTheme, String)
case file(Int32, PresentationTheme, Message?)
@@ -62,6 +72,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
switch self {
case .selectFromGallery, .selectFromFiles:
return AttachmentFileSection.select.rawValue
+ case .savedHeader, .savedFile, .showMore:
+ return AttachmentFileSection.savedMusic.rawValue
case .recentHeader, .file:
return AttachmentFileSection.recent.rawValue
}
@@ -73,10 +85,16 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
return 0
case .selectFromFiles:
return 1
- case .recentHeader:
+ case .savedHeader:
return 2
- case let .file(index, _, _):
+ case let .savedFile(index, _, _):
return 3 + index
+ case .showMore:
+ return 9999
+ case .recentHeader:
+ return 10000
+ case let .file(index, _, _):
+ return 10001 + index
}
}
@@ -94,6 +112,24 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
} else {
return false
}
+ case let .savedHeader(lhsTheme, lhsText):
+ if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
+ case let .savedFile(lhsIndex, lhsTheme, lhsMessage):
+ if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
+ return true
+ } else {
+ return false
+ }
+ case let .showMore(lhsTheme, lhsText):
+ if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
+ return true
+ } else {
+ return false
+ }
case let .recentHeader(lhsTheme, lhsText):
if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@@ -124,6 +160,22 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openFiles()
})
+
+ case let .savedHeader(_, text):
+ return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
+ case let .savedFile(_, _, message):
+ let interaction = ListMessageItemInteraction(openMessage: { message, _ in
+ arguments.send(message)
+ return false
+ }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
+
+ let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
+ let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
+ return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
+ case let .showMore(theme, text):
+ return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: {
+ arguments.expandSavedMusic()
+ })
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .file(_, _, message):
@@ -135,22 +187,52 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
- return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, displayFileInfo: false, displayBackground: true, style: .blocks)
+ return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
}
}
}
-private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
+private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
guard !empty else {
return []
}
var entries: [AttachmentFileEntry] = []
- entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
+ if case .recent = mode {
+ entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
+ }
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
+ let listTitle: String
+ switch mode {
+ case .recent:
+ listTitle = presentationData.strings.Attachment_RecentlySentFiles
+ case .audio:
+ listTitle = presentationData.strings.Attachment_SharedAudio
+ }
+
+ if case .audio = mode {
+ if let savedMusic, savedMusic.count > 0 {
+ entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased()))
+ var savedMusic = savedMusic
+ var showMore = false
+ if savedMusic.count > 4 && !state.savedMusicExpanded {
+ savedMusic = Array(savedMusic.prefix(3))
+ showMore = true
+ }
+ var i: Int32 = 0
+ for file in savedMusic {
+ entries.append(.savedFile(i, presentationData.theme, file))
+ i += 1
+ }
+ if showMore {
+ entries.append(.showMore(presentationData.theme, "Show More"))
+ }
+ }
+ }
+
if let recentDocuments = recentDocuments {
if recentDocuments.count > 0 {
- entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
+ entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
var i: Int32 = 0
for file in recentDocuments {
entries.append(.file(i, presentationData.theme, file))
@@ -158,7 +240,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
}
}
} else {
- entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
+ entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
for i in 0 ..< 11 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
@@ -185,6 +267,8 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon
var delayDisappear = false
+ var hasBottomEdgeEffect = true
+
var resetForReuseImpl: () -> Void = {}
public func resetForReuse() {
self.resetForReuseImpl()
@@ -229,31 +313,41 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon
transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame)
topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
- let bottomEdgeEffectView: EdgeEffectView
- if let current = self.bottomEdgeEffectView {
- bottomEdgeEffectView = current
- } else {
- bottomEdgeEffectView = EdgeEffectView()
- self.view.addSubview(bottomEdgeEffectView)
- self.bottomEdgeEffectView = bottomEdgeEffectView
+ if self.hasBottomEdgeEffect {
+ let bottomEdgeEffectView: EdgeEffectView
+ if let current = self.bottomEdgeEffectView {
+ bottomEdgeEffectView = current
+ } else {
+ bottomEdgeEffectView = EdgeEffectView()
+ self.view.addSubview(bottomEdgeEffectView)
+ self.bottomEdgeEffectView = bottomEdgeEffectView
+ }
+
+ let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
+ transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
+ transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
+ bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
+ } else if let bottomEdgeEffectView = self.bottomEdgeEffectView {
+ bottomEdgeEffectView.removeFromSuperview()
}
-
- let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
- transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
- transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
- bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
}
}
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
+ var savedMusicExpanded: Bool
}
-public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController {
+public enum AttachmentFileControllerMode {
+ case recent
+ case audio
+}
+
+public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: AttachmentFileControllerMode = .recent, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController {
let actionsDisposable = DisposableSet()
- let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true)
- let stateValue = Atomic(value: AttachmentFileControllerState(searching: false))
+ let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true)
+ let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false))
let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
@@ -265,39 +359,87 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
var updateIsSearchingImpl: ((Bool) -> Void)?
let arguments = AttachmentFileControllerArguments(
context: context,
+ isAudio: mode == .audio,
openGallery: {
presentGallery()
},
openFiles: {
presentFiles()
},
+ expandSavedMusic: {
+ updateState { state in
+ var updatedState = state
+ updatedState.savedMusicExpanded = true
+ return updatedState
+ }
+ },
send: { message in
- let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
- |> `catch` { _ in
- return .single(.result([]))
- }
- |> mapToSignal { result -> Signal<[Message], NoError> in
- guard case let .result(result) = result else {
- return .complete()
+ if message.id.namespace == Namespaces.Message.Local {
+ if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
+ send(.standalone(media: file))
}
- return .single(result)
- }
- |> deliverOnMainQueue).startStandalone(next: { messages in
- if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
- send(.message(message: MessageReference(message), media: file))
+ } else {
+ let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
+ |> `catch` { _ in
+ return .single(.result([]))
}
- dismissImpl?()
- })
+ |> mapToSignal { result -> Signal<[Message], NoError> in
+ guard case let .result(result) = result else {
+ return .complete()
+ }
+ return .single(result)
+ }
+ |> deliverOnMainQueue).startStandalone(next: { messages in
+ if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
+ send(.message(message: MessageReference(message), media: file))
+ }
+ dismissImpl?()
+ })
+ }
}
)
- let recentDocuments: Signal<[Message]?, NoError> = .single(nil)
- |> then(
- context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
- |> map { result -> [Message]? in
- return result.0.messages
- }
- )
+ let recentDocuments: Signal<[Message]?, NoError>
+ let savedMusicContext: ProfileSavedMusicContext?
+ let savedMusic: Signal<[Message]?, NoError>
+ switch mode {
+ case .recent:
+ recentDocuments = .single(nil)
+ |> then(
+ context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
+ |> map { result -> [Message]? in
+ return result.0.messages
+ }
+ )
+ savedMusicContext = nil
+ savedMusic = .single(nil)
+ case .audio:
+ recentDocuments = .single(nil)
+ |> then(
+ context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil)
+ |> map { result -> [Message]? in
+ return result.0.messages
+ }
+ )
+ savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId)
+ savedMusic = .single(nil)
+ |> then(
+ savedMusicContext!.state
+ |> map { state in
+ let peerId = context.account.peerId
+ var messages: [Message] = []
+ let peers = SimpleDictionary()
+// if let peer {
+// peers[peerId] = peer._asPeer()
+// }
+ for file in state.files {
+ let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
+ messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
+ }
+ return messages
+ }
+ )
+ }
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
@@ -306,14 +448,12 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
let previousRecentDocuments = Atomic<[Message]?>(value: nil)
let signal = combineLatest(queue: Queue.mainQueue(),
- presentationData,
- recentDocuments,
- statePromise.get()
- )
- |> map {
presentationData,
recentDocuments,
- state -> (ItemListControllerState, (ItemListNodeState, Any)) in
+ savedMusic,
+ statePromise.get()
+ )
+ |> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
@@ -397,9 +537,17 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
}
+ let title: String
+ switch mode {
+ case .recent:
+ title = presentationData.strings.Attachment_File
+ case .audio:
+ title = "Audio"
+ }
+
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
- title: .text(presentationData.strings.Attachment_File),
+ title: .text(title),
leftNavigationButton: leftNavigationButton,
rightNavigationButton: rightNavigationButton,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
@@ -424,7 +572,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
var searchItem: ItemListControllerSearch?
if state.searching {
- searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, focus: {
+ searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: {
expandImpl?()
}, cancel: {
updateState { state in
@@ -441,14 +589,18 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
})
}
- let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
+ let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
+ let _ = savedMusicContext?.state
}
let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true)
+ if case .audio = mode {
+ controller.hasBottomEdgeEffect = false
+ }
controller.delayDisappear = true
controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
switch offset {
@@ -490,3 +642,34 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
}
return controller
}
+
+public func storyAudioPickerController(
+ context: AccountContext,
+ selectFromFiles: @escaping () -> Void,
+ dismissed: @escaping () -> Void,
+ completion: @escaping (AnyMediaReference) -> Void,
+) -> ViewController {
+ var dismissImpl: (() -> Void)?
+ let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
+ let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData))
+ let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
+ controller.requestController = { _, present in
+ let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: {
+ selectFromFiles()
+ dismissImpl?()
+ }, send: { file in
+ completion(file)
+ dismissImpl?()
+ }) as! AttachmentFileControllerImpl
+ present(filePickerController, filePickerController.mediaPickerContext)
+ }
+ controller.navigationPresentation = .flatModal
+ controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
+ controller.didDismiss = {
+ dismissed()
+ }
+ dismissImpl = { [weak controller] in
+ controller?.dismiss(animated: true)
+ }
+ return controller
+}
diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift
index 3eaed92670..d1b6135765 100644
--- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift
+++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift
@@ -21,6 +21,7 @@ import SearchInputPanelComponent
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
+ let mode: AttachmentFileControllerMode
let presentationData: PresentationData
let focus: () -> Void
let cancel: () -> Void
@@ -31,8 +32,9 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
private var activity: ValuePromise = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
- init(context: AccountContext, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
+ init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
+ self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
@@ -69,7 +71,7 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
- return AttachmentFileSearchItemNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
+ return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, dismissInput: self.dismissInput)
}
@@ -77,8 +79,8 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
- private let theme: PresentationTheme
- private let strings: PresentationStrings
+ private let mode: AttachmentFileControllerMode
+ private let presentationData: PresentationData
private let focus: () -> Void
private let cancel: () -> Void
@@ -88,14 +90,14 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private var validLayout: ContainerViewLayout?
- init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
+ init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
- self.theme = theme
- self.strings = strings
+ self.mode = mode
+ self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
- self.containerNode = AttachmentFileSearchContainerNode(context: context, forceTheme: nil, send: { message in
+ self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in
send(message)
}, updateActivity: updateActivity)
@@ -140,11 +142,11 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
transition: .immediate,
component: AnyComponent(
SearchInputPanelComponent(
- theme: self.theme,
- strings: self.strings,
+ theme: self.presentationData.theme,
+ strings: self.presentationData.strings,
metrics: layout.metrics,
safeInsets: layout.safeInsets,
- placeholder: self.strings.Attachment_FilesSearchPlaceholder,
+ placeholder: self.mode == .audio ? self.presentationData.strings.Attachment_FilesSearchPlaceholder : self.presentationData.strings.Attachment_FilesSearchPlaceholder,
updated: { [weak self] query in
guard let self else {
return
@@ -251,7 +253,7 @@ private final class AttachmentFileSearchEntry: Comparable, Identifiable {
interaction.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
- return ListMessageItem(presentationData: ChatPresentationData(presentationData: interaction.context.sharedContext.currentPresentationData.with({$0})), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
+ return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
}
}
@@ -293,10 +295,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
- private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
-
private let presentationDataPromise: Promise
private var _hasDim: Bool = false
@@ -304,17 +304,12 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return _hasDim
}
- public init(context: AccountContext, forceTheme: PresentationTheme?, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
+ public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
self.context = context
self.send = send
- let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
- self.forceTheme = forceTheme
- if let forceTheme = self.forceTheme {
- self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
- }
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
@@ -380,13 +375,26 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
return .single(nil)
}
- let signal: Signal<[Message]?, NoError> = .single(nil)
- |> then(
- context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
- |> map { result -> [Message]? in
- return result.0.messages
- }
- )
+ let signal: Signal<[Message]?, NoError>
+ switch mode {
+ case .recent:
+ signal = .single(nil)
+ |> then(
+ context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
+ |> map { result -> [Message]? in
+ return result.0.messages
+ }
+ )
+ case .audio:
+ signal = .single(nil)
+ |> then(
+ context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil)
+ |> map { result -> [Message]? in
+ return result.0.messages
+ }
+ )
+ }
+
updateActivity(true)
return combineLatest(signal, presentationDataPromise.get())
@@ -420,25 +428,19 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon
}
}))
- self.presentationDataDisposable = (context.sharedContext.presentationData
- |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
- if let strongSelf = self {
- var presentationData = presentationData
-
- let previousTheme = strongSelf.presentationData.theme
- let previousStrings = strongSelf.presentationData.strings
-
- if let forceTheme = strongSelf.forceTheme {
- presentationData = presentationData.withUpdated(theme: forceTheme)
- }
-
- strongSelf.presentationData = presentationData
-
- if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
- strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
- }
- }
- })
+// self.presentationDataDisposable = (context.sharedContext.presentationData
+// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
+// if let strongSelf = self {
+// let previousTheme = strongSelf.presentationData.theme
+// let previousStrings = strongSelf.presentationData.strings
+//
+// strongSelf.presentationData = presentationData
+//
+// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
+// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
+// }
+// }
+// })
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD
index 7e0d886f68..cfc119906d 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD
@@ -80,6 +80,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
+ "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode",
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
index bcbf47663a..2d11aad170 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift
@@ -70,6 +70,7 @@ import ChatMessageStoryMentionContentNode
import ChatMessageUnsupportedBubbleContentNode
import ChatMessageWallpaperBubbleContentNode
import ChatMessageGiftBubbleContentNode
+import ChatMessageGiftOfferBubbleContentNode
import ChatMessageGiveawayBubbleContentNode
import ChatMessageJoinedChannelBubbleContentNode
import ChatMessageFactCheckBubbleContentNode
@@ -246,6 +247,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .suggestedBirthday = action.action {
result.append((message, ChatMessageBirthdateSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
+ } else if case .starGiftPurchaseOffer = action.action {
+ result.append((message, ChatMessageGiftOfferBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else {
if !canAddMessageReactions(message: message) {
needReactions = false
@@ -2821,6 +2824,47 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
actionButtonsFinalize = buttonsLayout
lastNodeTopPosition = .None(.Both)
+ } else if incoming, let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(_, _, expireDate, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined {
+ let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
+ if expireDate <= currentTimestamp {
+
+ } else {
+ var buttonDeclineValue: UInt8 = 0
+ let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1))
+ var buttonApproveValue: UInt8 = 1
+ let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1))
+
+ let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [
+ buttonApprove: ChatMessageActionButtonsNode.CustomInfo(
+ isEnabled: true,
+ icon: .suggestedPostApprove
+ ),
+ buttonDecline: ChatMessageActionButtonsNode.CustomInfo(
+ isEnabled: true,
+ icon: .suggestedPostReject
+ )
+ ]
+ let (minWidth, buttonsLayout) = actionButtonsLayout(
+ item.context,
+ item.presentationData.theme,
+ item.presentationData.chatBubbleCorners,
+ item.presentationData.strings,
+ item.controllerInteraction.presentationContext.backgroundNode,
+ ReplyMarkupMessageAttribute(
+ rows: [
+ ReplyMarkupRow(buttons: [
+ ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
+ ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
+ ])
+ ],
+ flags: [],
+ placeholder: nil
+ ), customInfos, item.message, baseWidth)
+ maxContentWidth = max(maxContentWidth, minWidth)
+ actionButtonsFinalize = buttonsLayout
+
+ lastNodeTopPosition = .None(.Both)
+ }
} else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil {
var canApprove = true
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) {
@@ -4772,7 +4816,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
let actionButtonsNode = actionButtonsSizeAndApply.1(animation)
- let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
+ var actionButtonsOriginX = backgroundFrame.minX
+ if case .center = alignment {
+ actionButtonsOriginX += 3.0
+ } else {
+ actionButtonsOriginX += incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right
+ }
+ let actionButtonsFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0)
if actionButtonsNode !== strongSelf.actionButtonsNode {
strongSelf.actionButtonsNode = actionButtonsNode
actionButtonsNode.buttonPressed = { [weak strongSelf] button, progress in
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift
index 7ebfa132a8..730e74f103 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift
@@ -280,9 +280,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
- case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
+ case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
- case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
+ case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
default:
break
@@ -430,6 +430,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
var buttonIcon: String?
var ribbonTitle = ""
+ var customRibbonColors: [UIColor]?
var textSpacing: CGFloat = 0.0
var isStarGift = false
@@ -554,7 +555,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
}
- case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, toPeerId):
+ case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, toPeerId, _):
var incoming = incoming
var convertStars = convertStars
if case let .generic(gift) = gift {
@@ -686,7 +687,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
- case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _):
+ case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer):
if case let .unique(uniqueGift) = gift {
isStarGift = true
@@ -715,7 +716,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
}
text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**"
- ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
+ if fromOffer {
+ ribbonTitle = incoming ? "" : item.presentationData.strings.Notification_StarGift_Sold
+ customRibbonColors = [UIColor(rgb: 0xd9433a), UIColor(rgb: 0xff645b)]
+ } else {
+ ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift
+ }
buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View
modelTitle = item.presentationData.strings.Notification_StarGift_Model
backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop
@@ -897,10 +903,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
), textAlignment: .center)
- let (creatorButtonTitleLayout, creatorButtonTitleApply) = makeCreatorButtonTitleLayout(TextNodeLayoutArguments(attributedString: creatorButtonAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
+ let (creatorButtonTitleLayout, creatorButtonTitleApply) = makeCreatorButtonTitleLayout(TextNodeLayoutArguments(attributedString: creatorButtonAttributedString, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
if modelTitle == nil && !creatorButtonTitle.isEmpty {
- textSpacing += 28.0
+ textSpacing += creatorButtonTitleLayout.size.height + 13.0
}
giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0
@@ -909,7 +915,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
giftSize.height += 70.0
if !creatorButtonTitle.isEmpty {
- giftSize.height += 28.0
+ giftSize.height += creatorButtonTitleLayout.size.height + 13.0
+ //28.0
}
}
@@ -1073,7 +1080,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var attributesOffsetY: CGFloat = 0.0
- let creatorButtonSize = CGSize(width: creatorButtonTitleLayout.size.width + 18.0, height: 18.0)
+ let creatorButtonSize = CGSize(width: creatorButtonTitleLayout.size.width + 18.0, height: creatorButtonTitleLayout.size.height + 3.0)
let creatorButtonOriginY = modelTitle == nil ? titleFrame.maxY + 4.0 : clippingTextFrame.maxY + 5.0
let creatorButtonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - creatorButtonSize.width) / 2.0), y: creatorButtonOriginY), size: creatorButtonSize)
if !creatorButtonTitle.isEmpty {
@@ -1284,7 +1291,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if ribbonTextLayout.size.width > 0.0 {
if strongSelf.ribbonBackgroundNode.image == nil {
- if let uniqueBackgroundColor {
+ if let customRibbonColors {
+ strongSelf.ribbonBackgroundNode.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: customRibbonColors, direction: .mirroredDiagonal)
+ } else if let uniqueBackgroundColor {
let colors = [
uniqueBackgroundColor.withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89),
uniqueBackgroundColor.withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD
new file mode 100644
index 0000000000..db4d3ca487
--- /dev/null
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD
@@ -0,0 +1,36 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "ChatMessageGiftOfferBubbleContentNode",
+ module_name = "ChatMessageGiftOfferBubbleContentNode",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AsyncDisplayKit",
+ "//submodules/Display",
+ "//submodules/SSignalKit/SwiftSignalKit",
+ "//submodules/TelegramCore",
+ "//submodules/AccountContext",
+ "//submodules/TelegramPresentationData",
+ "//submodules/TelegramUIPreferences",
+ "//submodules/TextFormat",
+ "//submodules/LocalizedPeerData",
+ "//submodules/UrlEscaping",
+ "//submodules/TelegramStringFormatting",
+ "//submodules/WallpaperBackgroundNode",
+ "//submodules/ReactionSelectionNode",
+ "//submodules/TelegramUI/Components/ChatControllerInteraction",
+ "//submodules/Markdown",
+ "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
+ "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
+ "//submodules/TelegramUI/Components/TextNodeWithEntities",
+ "//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift
new file mode 100644
index 0000000000..f50e5b49c3
--- /dev/null
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift
@@ -0,0 +1,333 @@
+import Foundation
+import UIKit
+import AsyncDisplayKit
+import Display
+import SwiftSignalKit
+import ComponentFlow
+import TelegramCore
+import AccountContext
+import TelegramPresentationData
+import TelegramUIPreferences
+import TextFormat
+import LocalizedPeerData
+import UrlEscaping
+import TelegramStringFormatting
+import WallpaperBackgroundNode
+import ReactionSelectionNode
+import AnimatedStickerNode
+import TelegramAnimatedStickerNode
+import ChatControllerInteraction
+import ShimmerEffect
+import Markdown
+import ChatMessageBubbleContentNode
+import ChatMessageItemCommon
+import TextNodeWithEntities
+import GiftItemComponent
+
+private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
+ return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
+}
+
+public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode {
+ private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
+ private let titleNode: TextNode
+ private let subtitleNode: TextNodeWithEntities
+ private let giftIcon = ComponentView()
+
+ private var absoluteRect: (CGRect, CGSize)?
+
+ private var isPlaying: Bool = false
+
+ override public var disablesClipping: Bool {
+ return true
+ }
+
+ override public var visibility: ListViewItemNodeVisibility {
+ didSet {
+ let wasVisible = oldValue != .none
+ let isVisible = self.visibility != .none
+
+ if wasVisible != isVisible {
+ self.visibilityStatus = isVisible
+
+ switch self.visibility {
+ case .none:
+ self.subtitleNode.visibilityRect = nil
+ case let .visible(_, subRect):
+ var subRect = subRect
+ subRect.origin.x = 0.0
+ subRect.size.width = 10000.0
+ self.subtitleNode.visibilityRect = subRect
+ }
+ }
+ }
+ }
+
+ private var visibilityStatus: Bool? {
+ didSet {
+ if self.visibilityStatus != oldValue {
+ self.updateVisibility()
+ }
+ }
+ }
+
+ private var fetchDisposable: Disposable?
+ private var setupTimestamp: Double?
+
+ private var cachedTonImage: (UIImage, UIColor)?
+
+ required public init() {
+ self.titleNode = TextNode()
+ self.titleNode.isUserInteractionEnabled = false
+ self.titleNode.displaysAsynchronously = false
+
+ self.subtitleNode = TextNodeWithEntities()
+ self.subtitleNode.textNode.isUserInteractionEnabled = false
+ self.subtitleNode.textNode.displaysAsynchronously = false
+
+ super.init()
+
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.subtitleNode.textNode)
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ deinit {
+ self.fetchDisposable?.dispose()
+ }
+
+ override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
+ let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
+ let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
+
+ return { item, layoutConstants, _, _, _, _ in
+ let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
+
+ return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
+ var giftSize = CGSize(width: 260.0, height: 240.0)
+ var uniqueGift: StarGift.UniqueGift?
+
+ let incoming: Bool
+ if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil {
+ incoming = true
+ } else {
+ incoming = item.message.effectivelyIncoming(item.context.account.peerId)
+ }
+
+ let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
+
+ let text: String
+ let additionalText: String
+
+ var hasActionButtons = false
+ if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action {
+ let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
+
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = item.presentationData.strings.Notification_StarGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
+ let giftTitle: String
+ if case let .unique(gift) = gift {
+ giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))"
+ uniqueGift = gift
+ } else {
+ giftTitle = ""
+ }
+
+ if incoming {
+ text = item.presentationData.strings.Notification_StarGiftOffer_Offer(peerName, priceString, giftTitle).string
+ } else {
+ text = item.presentationData.strings.Notification_StarGiftOffer_OfferYou(peerName, priceString, giftTitle).string
+ }
+
+ if isAccepted {
+ additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Accepted
+ } else if isDeclined {
+ additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Rejected
+ } else if expireDate > currentTimestamp {
+ func textForTimeout(_ value: Int32) -> String {
+ if value < 3600 {
+ let minutes = value / 60
+ return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes)
+ } else {
+ let hours = value / 3600
+ let minutes = (value % 3600) / 60
+ return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Hours(hours) + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Delimiter + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes)
+ }
+ }
+ let delta = expireDate - currentTimestamp
+ additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expires(textForTimeout(delta)).string
+
+ if incoming {
+ hasActionButtons = true
+ }
+ } else {
+ additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expired
+ }
+ } else {
+ text = ""
+ additionalText = ""
+ }
+
+ let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center))
+
+ let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
+
+ let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
+ link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
+ linkAttribute: { url in
+ return ("URL", url)
+ }
+ ), textAlignment: .center)
+
+ let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
+ let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
+
+ giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0
+
+ let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0)
+
+ return (backgroundSize.width, { boundingWidth in
+ return (backgroundSize, { [weak self] animation, synchronousLoads, info in
+ if let strongSelf = self {
+ strongSelf.item = item
+
+ let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize)
+ let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
+
+ strongSelf.updateVisibility()
+
+ let _ = titleApply()
+ let _ = subtitleApply(TextNodeWithEntities.Arguments(
+ context: item.context,
+ cache: item.controllerInteraction.presentationContext.animationCache,
+ renderer: item.controllerInteraction.presentationContext.animationRenderer,
+ placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
+ attemptSynchronous: synchronousLoads
+ ))
+
+
+ let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size)
+ strongSelf.subtitleNode.textNode.frame = textFrame
+
+ let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size)
+ strongSelf.titleNode.frame = titleFrame
+
+ if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
+ backgroundContent.clipsToBounds = true
+ backgroundContent.cornerRadius = 24.0
+
+ strongSelf.mediaBackgroundContent = backgroundContent
+ strongSelf.insertSubnode(backgroundContent, at: 0)
+ }
+
+ if let backgroundContent = strongSelf.mediaBackgroundContent {
+ animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
+ backgroundContent.clipsToBounds = true
+
+ if hasActionButtons {
+ backgroundContent.cornerRadius = 0.0
+ if backgroundContent.view.mask == nil {
+ backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in
+ context.clear(CGRect(origin: .zero, size: size))
+ context.setFillColor(UIColor.white.cgColor)
+
+ context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil))
+ context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil))
+ context.fillPath()
+ }))
+ }
+ } else {
+ backgroundContent.view.mask = nil
+ backgroundContent.cornerRadius = 24.0
+ }
+ }
+
+ if let uniqueGift {
+ let iconSize = CGSize(width: 94.0, height: 94.0)
+ let _ = strongSelf.giftIcon.update(
+ transition: .immediate,
+ component: AnyComponent(GiftItemComponent(
+ context: item.context,
+ theme: item.presentationData.theme.theme,
+ strings: item.presentationData.strings,
+ peer: nil,
+ subject: .uniqueGift(gift: uniqueGift, price: nil),
+ mode: .thumbnail
+ )),
+ environment: {},
+ containerSize: iconSize
+ )
+ if let giftIconView = strongSelf.giftIcon.view {
+ if giftIconView.superview == nil {
+ strongSelf.view.addSubview(giftIconView)
+ }
+ giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize)
+ }
+ }
+
+ if let (rect, size) = strongSelf.absoluteRect {
+ strongSelf.updateAbsoluteRect(rect, within: size)
+ }
+
+ switch strongSelf.visibility {
+ case .none:
+ strongSelf.subtitleNode.visibilityRect = nil
+ case let .visible(_, subRect):
+ var subRect = subRect
+ subRect.origin.x = 0.0
+ subRect.size.width = 10000.0
+ strongSelf.subtitleNode.visibilityRect = subRect
+ }
+ }
+ })
+ })
+ })
+ }
+ }
+
+ override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
+ self.absoluteRect = (rect, containerSize)
+
+ if let mediaBackgroundContent = self.mediaBackgroundContent {
+ var backgroundFrame = mediaBackgroundContent.frame
+ backgroundFrame.origin.x += rect.minX
+ backgroundFrame.origin.y += rect.minY
+ mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
+ }
+ }
+
+ override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
+
+ }
+
+ override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
+
+ }
+
+ override public func unreadMessageRangeUpdated() {
+ self.updateVisibility()
+ }
+
+ override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
+ if self.mediaBackgroundContent?.frame.contains(point) == true {
+ return ChatMessageBubbleContentTapAction(content: .openMessage)
+ } else {
+ return ChatMessageBubbleContentTapAction(content: .none)
+ }
+ }
+
+ private func updateVisibility() {
+ }
+}
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD
index cf18fe2633..555ad4a0a5 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD
@@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
+ "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/AnimatedCountLabelNode",
"//submodules/AudioWaveform",
"//submodules/DeviceProximity",
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
index 60857c7020..550414b478 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift
@@ -35,6 +35,7 @@ import TelegramStringFormatting
import AnimatedCountLabelNode
import AudioWaveform
import DeviceProximity
+import ShimmeringLinkNode
private struct FetchControls {
let fetch: (Bool) -> Void
@@ -123,6 +124,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
public let fetchingTextNode: ImmediateTextNode
public let fetchingCompactTextNode: ImmediateTextNode
private let countNode: ImmediateAnimatedCountLabelNode
+ private var shimmeringNodes: [ShimmeringLinkNode] = []
public var waveformView: ComponentHostView?
@@ -809,23 +811,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var displayTrailingAnimatedDots = false
+ var isTranslating = false
if let transcribedText = transcribedText, case .expanded = effectiveAudioTranscriptionState {
switch transcribedText {
case let .success(text, isPending):
textString = NSAttributedString(string: text, font: textFont, textColor: messageTheme.primaryTextColor)
- /*#if DEBUG
- var isPending = isPending
- if "".isEmpty {
- isPending = true
- }
- #endif*/
-
if isPending {
let modifiedString = NSMutableAttributedString(attributedString: textString!)
modifiedString.append(NSAttributedString(string: "...", font: textFont, textColor: .clear))
displayTrailingAnimatedDots = true
textString = modifiedString
+ } else {
+ if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty && arguments.incoming {
+ isTranslating = true
+ for attribute in arguments.message.attributes {
+ if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
+ textString = NSAttributedString(string: attribute.text, font: textFont, textColor: messageTheme.primaryTextColor)
+ isTranslating = false
+ break
+ }
+ }
+ }
}
case let .error(error):
let errorTextFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 15.0 / 17.0))
@@ -1538,6 +1545,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
+
+ strongSelf.updateIsTranslating(isTranslating)
}
})
})
@@ -1545,6 +1554,39 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
+ private func updateIsTranslating(_ isTranslating: Bool) {
+ guard let arguments = self.arguments else {
+ return
+ }
+ var rects: [[CGRect]] = []
+ let titleRects = (self.textNode.rangeRects(in: NSRange(location: 0, length: self.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { self.textNode.view.convert($0, to: self.textClippingNode.view) }
+ rects.append(titleRects)
+
+ if isTranslating, !rects.isEmpty {
+ if self.shimmeringNodes.isEmpty {
+ for rects in rects {
+ let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
+ shimmeringNode.updateRects(rects)
+ shimmeringNode.frame = self.bounds
+ shimmeringNode.updateLayout(self.bounds.size)
+ shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
+ self.shimmeringNodes.append(shimmeringNode)
+ self.textClippingNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode)
+ }
+ }
+ } else if !self.shimmeringNodes.isEmpty {
+ let shimmeringNodes = self.shimmeringNodes
+ self.shimmeringNodes = []
+
+ for shimmeringNode in shimmeringNodes {
+ shimmeringNode.alpha = 0.0
+ shimmeringNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmeringNode] _ in
+ shimmeringNode?.removeFromSupernode()
+ })
+ }
+ }
+ }
+
private func updateStatus(animated: Bool) {
guard let resourceStatus = self.resourceStatus else {
return
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift
index bb8e7164d3..730327cfaa 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift
@@ -2223,7 +2223,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
giftView = ComponentView()
strongSelf.giftView = giftView
}
-
+
let _ = giftView.update(
transition: .immediate,
component: AnyComponent(
@@ -2231,7 +2231,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
context: context,
theme: presentationData.theme.theme,
strings: presentationData.strings,
- subject: .auction(gift: gift, centerColor: giftPreview.centerColor, edgeColor: giftPreview.edgeColor, endTime: giftPreview.endTime),
+ subject: .auction(gift: gift, endTime: giftPreview.endTime),
title: gift.title ?? "",
subtitle: presentationData.strings.Chat_Auction_Gifts(gift.availability?.total ?? 0),
mode: .preview
diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift
index 266090f867..8b1c19f5e2 100644
--- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift
@@ -346,7 +346,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
case "telegram_auction":
for attribute in webpage.attributes {
if case let .giftAuction(giftAuction) = attribute, case let .generic(gift) = giftAuction.gift {
- let media = GiftAuctionPreviewMedia(content: gift, centerColor: UIColor(rgb: UInt32(bitPattern: giftAuction.centerColor)), edgeColor: UIColor(rgb: UInt32(bitPattern: giftAuction.edgeColor)), endTime: giftAuction.endDate)
+ let media = GiftAuctionPreviewMedia(content: gift, endTime: giftAuction.endDate)
mediaAndFlags = ([media], [])
break
}
@@ -490,19 +490,29 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
actionTitle = item.presentationData.strings.Chat_ViewAlbum
case "telegram_auction":
var hasEnded = false
+ var isUpcoming = false
+ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
for attribute in webpage.attributes {
if case let .giftAuction(giftAuction) = attribute {
- let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
+ if case let .generic(gift) = giftAuction.gift, let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate {
+ isUpcoming = true
+ }
if giftAuction.endDate < currentTime {
hasEnded = true
}
break
}
}
- subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction, font: titleFont)
text = nil
- actionTitle = hasEnded ? item.presentationData.strings.Chat_Auction_ViewResults : item.presentationData.strings.Chat_Auction_Join
- actionIcon = !hasEnded ? .bid : nil
+ if isUpcoming {
+ subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction_Upcoming, font: titleFont)
+ actionTitle = item.presentationData.strings.Chat_Auction_View
+ actionIcon = nil
+ } else {
+ subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction, font: titleFont)
+ actionTitle = hasEnded ? item.presentationData.strings.Chat_Auction_ViewResults : item.presentationData.strings.Chat_Auction_Join
+ actionIcon = !hasEnded ? .bid : nil
+ }
default:
break
}
diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD
index 1d1499967b..393edc5f09 100644
--- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD
+++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD
@@ -45,6 +45,9 @@ swift_library(
"//submodules/AnimatedCountLabelNode",
"//submodules/HexColor",
"//submodules/QrCodeUI",
+ "//submodules/TelegramUI/Components/GlassBarButtonComponent",
+ "//submodules/Components/BundleIconComponent",
+ "//submodules/Components/SheetComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift
index 6b69db21d9..8ada9fca44 100644
--- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift
+++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift
@@ -36,6 +36,10 @@ import SegmentedControlNode
import AnimatedCountLabelNode
import HexColor
import QrCodeUI
+import ComponentFlow
+import GlassBarButtonComponent
+import SheetComponent
+import BundleIconComponent
private func closeButtonImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@@ -717,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
}
private func iconColors(theme: PresentationTheme) -> [String: UIColor] {
- let accentColor = theme.actionSheet.controlAccentColor
+ let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor
var colors: [String: UIColor] = [:]
colors["Sunny.Path 14.Path.Stroke 1"] = accentColor
colors["Sunny.Path 15.Path.Stroke 1"] = accentColor
@@ -745,18 +749,6 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f
private let defaultEmoticon = "🏠"
-private func generateShadowImage() -> UIImage? {
- return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
- context.clear(CGRect(origin: CGPoint(), size: size))
-
- context.setShadow(offset: CGSize(width: 0.0, height: -0.5), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
- context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
- let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)), cornerRadius: 16.0)
- context.addPath(path.cgPath)
- context.fillPath()
- })?.stretchableImage(withLeftCapWidth: 20, topCapHeight: 0)
-}
-
private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
@@ -768,14 +760,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
private let scrollNodeContentNode: ASDisplayNode
private let contentContainerNode: ASDisplayNode
private let topContentContainerNode: SparseNode
- private let shadowNode: ASImageNode
- private let effectNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
- private let contentBackgroundNode: ASDisplayNode
+ private let contentBackgroundView: SheetBackgroundView
+ private let cancelButton = ComponentView()
+ private let switchThemeButton = ComponentView()
private let titleNode: ASTextNode
private let segmentedNode: SegmentedControlNode
- private let cancelButton: HighlightableButtonNode
- private let switchThemeButton: HighlightTrackingButtonNode
+ private let cancelButtonNode: HighlightableButtonNode
+ private let switchThemeButtonNode: HighlightTrackingButtonNode
private let animationContainerNode: ASDisplayNode
private var animationNode: AnimationNode
private let doneButton: SolidRoundedButtonNode
@@ -841,28 +833,17 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.topContentContainerNode = SparseNode()
self.topContentContainerNode.isOpaque = false
-
- self.shadowNode = ASImageNode()
- self.shadowNode.contentMode = .scaleToFill
- self.shadowNode.image = generateShadowImage()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
- self.backgroundNode.cornerRadius = 16.0
+ self.backgroundNode.cornerRadius = 38.0
self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance
self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance)
- let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
let textColor = self.presentationData.theme.actionSheet.primaryTextColor
- let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
-
- self.effectNode = ASDisplayNode(viewBlock: {
- return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
- })
-
- self.contentBackgroundNode = ASDisplayNode()
- self.contentBackgroundNode.backgroundColor = backgroundColor
+
+ self.contentBackgroundView = SheetBackgroundView()
self.titleNode = ASTextNode()
let title: String
@@ -878,12 +859,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.segmentedNode.isHidden = !self.contentNode.hasVideo
self.titleNode.isHidden = !self.segmentedNode.isHidden
- self.cancelButton = HighlightableButtonNode()
- self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
- self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Close
- self.cancelButton.accessibilityTraits = [.button]
+ self.cancelButtonNode = HighlightableButtonNode()
+ self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
+ self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Close
+ self.cancelButtonNode.accessibilityTraits = [.button]
- self.switchThemeButton = HighlightTrackingButtonNode()
+ self.switchThemeButtonNode = HighlightTrackingButtonNode()
self.animationContainerNode = ASDisplayNode()
self.animationContainerNode.isUserInteractionEnabled = false
@@ -916,13 +897,10 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.scrollNodeContentNode.addSubnode(self.contentNode)
- self.scrollNodeContentNode.addSubnode(self.shadowNode)
self.scrollNodeContentNode.addSubnode(self.backgroundNode)
self.scrollNodeContentNode.addSubnode(self.contentContainerNode)
self.scrollNodeContentNode.addSubnode(self.topContentContainerNode)
- self.backgroundNode.addSubnode(self.effectNode)
- self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.segmentedNode)
self.contentContainerNode.addSubnode(self.doneButton)
@@ -930,12 +908,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.topContentContainerNode.addSubnode(self.animationContainerNode)
self.animationContainerNode.addSubnode(self.animationNode)
- self.topContentContainerNode.addSubnode(self.switchThemeButton)
+ //self.topContentContainerNode.addSubnode(self.switchThemeButtonNode)
self.topContentContainerNode.addSubnode(self.listNode)
- self.topContentContainerNode.addSubnode(self.cancelButton)
+ //self.topContentContainerNode.addSubnode(self.cancelButtonNode)
- self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside)
- self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
+ self.switchThemeButtonNode.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside)
+ self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.segmentedNode.selectedIndexChanged = { [weak self] index in
guard let strongSelf = self, let contentNode = strongSelf.contentNode as? MessageContentNode, let videoNode = contentNode.videoNode else {
@@ -1181,7 +1159,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
}
}))
- self.switchThemeButton.highligthedChanged = { [weak self] highlighted in
+ self.switchThemeButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity")
@@ -1280,14 +1258,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
- self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
+ self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal)
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor))
let previousIconColors = iconColors(theme: previousTheme)
let newIconColors = iconColors(theme: self.presentationData.theme)
- if !self.switchThemeButton.isUserInteractionEnabled {
+ if !self.switchThemeButtonNode.isUserInteractionEnabled {
let themeCrossfadeDuration: Double = 0.3
let themeCrossfadeDelay: Double = 0.25
@@ -1312,6 +1290,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
+ self.backgroundNode.view.addSubview(self.contentBackgroundView)
+
self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true
}
@@ -1320,9 +1300,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
}
@objc private func switchThemePressed() {
- self.switchThemeButton.isUserInteractionEnabled = false
+ self.switchThemeButtonNode.isUserInteractionEnabled = false
Queue.mainQueue().after(0.5) {
- self.switchThemeButton.isUserInteractionEnabled = true
+ self.switchThemeButtonNode.isUserInteractionEnabled = true
}
self.animateCrossfade(animateIcon: false)
@@ -1372,18 +1352,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
})
}
- Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) {
- if let effectView = self.effectNode.view as? UIVisualEffectView {
- UIView.animate(withDuration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) {
- effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
- } completion: { _ in
- }
- }
-
- let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
- self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
- self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration)
- }
+// Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) {
+// let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
+// self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
+// self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration)
+// }
if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.contentContainerNode.frame
@@ -1403,7 +1376,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
private var animatedOut = false
public func animateIn() {
- let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
+ let offset = self.bounds.size.height - self.contentBackgroundView.frame.minY
if let (layout, _) = self.containerLayout {
self.scrollNodeContentNode.cornerRadius = layout.deviceMetrics.screenCornerRadius
@@ -1424,7 +1397,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
self.wrappingScrollNode.view.isScrollEnabled = false
- let distance = self.bounds.size.height - self.contentBackgroundNode.frame.minY
+ let distance = self.bounds.size.height - self.contentBackgroundView.frame.minY
if let velocity {
let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity / distance)
self.wrappingScrollNode.layer.animateSpring(from: 0.0 as NSNumber, to: -distance as NSNumber, keyPath: "bounds.origin.y", duration: 0.45, delay: 0.0, initialVelocity: initialVelocity, damping: 124.0, removeOnCompletion: false, additive: true, completion: { _ in
@@ -1473,24 +1446,81 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0)
let sideInset = floor((layout.size.width - width) / 2.0)
- let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
+ let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight - 6.0), size: CGSize(width: width, height: contentHeight)).insetBy(dx: 6.0, dy: 0.0)
let contentFrame = contentContainerFrame
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
-
- let shadowFrame = CGRect(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0, width: backgroundFrame.width, height: 40.0)
- transition.updateFrame(node: self.shadowNode, frame: shadowFrame)
+
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
- transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
- transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
+
+ self.contentBackgroundView.update(size: contentFrame.size, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, transition: .immediate)
+ transition.updateFrame(view: self.contentBackgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
+
+ let barButtonSize = CGSize(width: 40.0, height: 40.0)
+ let cancelButtonSize = self.cancelButton.update(
+ transition: .immediate,
+ component: AnyComponent(GlassBarButtonComponent(
+ size: barButtonSize,
+ backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
+ isDark: self.presentationData.theme.overallDarkAppearance,
+ state: .generic,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
+ )
+ )),
+ action: { [weak self] _ in
+ self?.cancelButtonPressed()
+ }
+ )),
+ environment: {},
+ containerSize: barButtonSize
+ )
+ let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize)
+ if let view = self.cancelButton.view {
+ if view.superview == nil {
+ self.topContentContainerNode.view.addSubview(view)
+ }
+ view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size)
+ view.center = cancelButtonFrame.center
+ }
+
+ let switchThemeButtonSize = self.switchThemeButton.update(
+ transition: .immediate,
+ component: AnyComponent(GlassBarButtonComponent(
+ size: barButtonSize,
+ backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
+ isDark: self.presentationData.theme.overallDarkAppearance,
+ state: .generic,
+ component: AnyComponentWithIdentity(id: "switchTheme", component: AnyComponent(
+ Rectangle(color: .clear)
+ )),
+ action: { [weak self] _ in
+ self?.switchThemePressed()
+ }
+ )),
+ environment: {},
+ containerSize: barButtonSize
+ )
+ let switchThemeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeButtonSize.width - 16.0, y: 16.0), size: switchThemeButtonSize)
+ if let view = self.switchThemeButton.view {
+ if view.superview == nil {
+ self.topContentContainerNode.view.addSubview(view)
+ self.topContentContainerNode.view.bringSubviewToFront(self.animationContainerNode.view)
+ }
+ view.bounds = CGRect(origin: .zero, size: switchThemeButtonFrame.size)
+ view.center = switchThemeButtonFrame.center
+ }
+
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height + 2000.0)))
let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight))
- let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize)
+ let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 36.0 - titleSize.height / 2.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let segmentedSize = self.segmentedNode.updateLayout(.sizeToFit(maximumWidth: width - 90.0, minimumWidth: 160.0, height: 32.0), transition: transition)
@@ -1498,20 +1528,20 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let switchThemeSize = CGSize(width: 44.0, height: 44.0)
let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize)
- transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame)
- transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0))
+ transition.updateFrame(node: self.switchThemeButtonNode, frame: switchThemeFrame)
+ transition.updateFrame(node: self.animationContainerNode, frame: switchThemeButtonFrame.insetBy(dx: 5.0, dy: 5.0))
transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size))
let cancelSize = CGSize(width: 44.0, height: 44.0)
let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize)
- transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
+ transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame)
- let buttonInset: CGFloat = 16.0
+ let buttonInset: CGFloat = 30.0
let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
- transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: scanButtonHeight))
+ transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom, width: contentFrame.width, height: scanButtonHeight))
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
- transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight))
+ transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom, width: contentFrame.width, height: doneButtonHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame)
@@ -1523,7 +1553,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
let contentSize = CGSize(width: contentFrame.width, height: 120.0)
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width)
- self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0)
+ self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 12.0)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.contentNode.updateLayout(size: layout.size, topInset: 44.0, bottomInset: contentHeight, transition: transition)
diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift
index 01d0a629d1..9c2f87ad96 100644
--- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift
+++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift
@@ -1327,7 +1327,7 @@ public final class EmojiStatusSelectionController: ViewController {
return
}
if !self.context.isPremium || count < 3, let pushController = controller.pushController {
- let controller = self.context.sharedContext.makeGiftWearPreviewScreen(context: self.context, gift: gift)
+ let controller = self.context.sharedContext.makeGiftWearPreviewScreen(context: self.context, gift: .unique(gift), attributes: nil)
pushController(controller)
} else {
let _ = (self.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: nil)
diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift
index 48dc56d531..bad7a1666f 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift
@@ -18,6 +18,10 @@ import UIKitRuntimeUtils
public final class GiftCompositionComponent: Component {
public class ExternalState {
public fileprivate(set) var previewPatternColor: UIColor?
+ public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute?
+ public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute?
+ public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute?
+
public init() {
self.previewPatternColor = nil
}
@@ -35,6 +39,8 @@ public final class GiftCompositionComponent: Component {
let animationOffset: CGPoint?
let animationScale: CGFloat?
let displayAnimationStars: Bool
+ let animateScaleOnTransition: Bool
+ let alwaysAnimateTransition: Bool
let revealedAttributes: Set
let externalState: ExternalState?
let requestUpdate: (ComponentTransition) -> Void
@@ -46,6 +52,8 @@ public final class GiftCompositionComponent: Component {
animationOffset: CGPoint? = nil,
animationScale: CGFloat? = nil,
displayAnimationStars: Bool = false,
+ animateScaleOnTransition: Bool = true,
+ alwaysAnimateTransition: Bool = false,
revealedAttributes: Set = Set(),
externalState: ExternalState? = nil,
requestUpdate: @escaping (ComponentTransition) -> Void = { _ in }
@@ -56,6 +64,8 @@ public final class GiftCompositionComponent: Component {
self.animationOffset = animationOffset
self.animationScale = animationScale
self.displayAnimationStars = displayAnimationStars
+ self.animateScaleOnTransition = animateScaleOnTransition
+ self.alwaysAnimateTransition = alwaysAnimateTransition
self.revealedAttributes = revealedAttributes
self.externalState = externalState
self.requestUpdate = requestUpdate
@@ -80,6 +90,9 @@ public final class GiftCompositionComponent: Component {
if lhs.displayAnimationStars != rhs.displayAnimationStars {
return false
}
+ if lhs.animateScaleOnTransition != rhs.animateScaleOnTransition {
+ return false
+ }
if lhs.revealedAttributes != rhs.revealedAttributes {
return false
}
@@ -671,18 +684,23 @@ public final class GiftCompositionComponent: Component {
loop = true
self.stopSpinIfNeeded()
- if self.previewModels.isEmpty {
+ if self.previewModels.isEmpty || sampleAttributes.count == 3 {
var models: [StarGift.UniqueGift.Attribute] = []
var patterns: [StarGift.UniqueGift.Attribute] = []
var backdrops: [StarGift.UniqueGift.Attribute] = []
for attribute in sampleAttributes {
switch attribute {
- case .model: models.append(attribute)
+ case .model: models.append(attribute)
case .pattern: patterns.append(attribute)
case .backdrop: backdrops.append(attribute)
default: break
}
}
+
+ if self.previewModels != models && sampleAttributes.count == 3 {
+ self.animatePreviewTransition = true
+ }
+
self.previewModels = models
self.previewPatterns = patterns
self.previewBackdrops = backdrops
@@ -707,19 +725,22 @@ public final class GiftCompositionComponent: Component {
}
if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] {
animationFile = file
+ component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)]
}
- if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] {
+ if !self.previewPatterns.isEmpty, case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] {
patternFile = file
files[file.fileId.id] = file
+ component.externalState?.previewSymbol = self.previewPatterns[Int(self.previewPatternIndex)]
}
- if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
+ if !self.previewBackdrops.isEmpty, case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] {
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
+ component.externalState?.previewBackdrop = self.previewBackdrops[Int(self.previewBackdropIndex)]
}
}
- if self.previewTimer == nil {
+ if self.previewTimer == nil && sampleAttributes.count > 3 {
self.previewTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in
guard let self, !self.previewModels.isEmpty else { return }
self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count)
@@ -755,7 +776,9 @@ public final class GiftCompositionComponent: Component {
}
var animateTransition = false
- if self.animatePreviewTransition {
+ if component.alwaysAnimateTransition {
+ animateTransition = true
+ } else if self.animatePreviewTransition {
animateTransition = true
self.animatePreviewTransition = false
} else if let previousComponent, case .preview = previousComponent.subject, case .unique = component.subject {
@@ -776,6 +799,9 @@ public final class GiftCompositionComponent: Component {
bounce = self.previewPatternIndex == -1
background = false
}
+ if !component.animateScaleOnTransition {
+ bounce = false
+ }
backgroundView.animateTransition(background: background, bounce: bounce)
}
if animateBackdropSwipe {
diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD
new file mode 100644
index 0000000000..152dfb21de
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD
@@ -0,0 +1,42 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "GiftDemoScreen",
+ module_name = "GiftDemoScreen",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AsyncDisplayKit",
+ "//submodules/Display",
+ "//submodules/Postbox",
+ "//submodules/TelegramCore",
+ "//submodules/SSignalKit/SwiftSignalKit",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ViewControllerComponent",
+ "//submodules/Components/ComponentDisplayAdapters",
+ "//submodules/Components/MultilineTextComponent",
+ "//submodules/Components/BalancedTextComponent",
+ "//submodules/TelegramPresentationData",
+ "//submodules/AccountContext",
+ "//submodules/AppBundle",
+ "//submodules/TelegramStringFormatting",
+ "//submodules/PresentationDataUtils",
+ "//submodules/Components/SheetComponent",
+ "//submodules/TextFormat",
+ "//submodules/TelegramUI/Components/ScrollComponent",
+ "//submodules/TelegramUI/Components/ButtonComponent",
+ "//submodules/TelegramUI/Components/GlassBarButtonComponent",
+ "//submodules/Components/BundleIconComponent",
+ "//submodules/PremiumUI",
+ "//submodules/TelegramUI/Components/LottieComponent",
+ "//submodules/TelegramUI/Components/EdgeEffect",
+ "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift
new file mode 100644
index 0000000000..a026d104eb
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift
@@ -0,0 +1,584 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import Postbox
+import TelegramCore
+import SwiftSignalKit
+import AccountContext
+import TelegramPresentationData
+import PresentationDataUtils
+import ComponentFlow
+import ViewControllerComponent
+import SheetComponent
+import MultilineTextComponent
+import BalancedTextComponent
+import BundleIconComponent
+import ButtonComponent
+import Markdown
+import GlassBarButtonComponent
+import PremiumUI
+import ScrollComponent
+import LottieComponent
+import EdgeEffect
+import InfoParagraphComponent
+
+final class PageComponent: CombinedComponent {
+ typealias EnvironmentType = ChildEnvironment
+
+ private let content: AnyComponent
+ private let title: String
+ private let text: String
+ private let textColor: UIColor
+
+ init(
+ content: AnyComponent,
+ title: String,
+ text: String,
+ textColor: UIColor
+ ) {
+ self.content = content
+ self.title = title
+ self.text = text
+ self.textColor = textColor
+ }
+
+ static func ==(lhs: PageComponent, rhs: PageComponent) -> Bool {
+ if lhs.content != rhs.content {
+ return false
+ }
+ if lhs.title != rhs.title {
+ return false
+ }
+ if lhs.text != rhs.text {
+ return false
+ }
+ if lhs.textColor != rhs.textColor {
+ return false
+ }
+ return true
+ }
+
+ static var body: Body {
+ let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
+ let title = Child(MultilineTextComponent.self)
+ let text = Child(MultilineTextComponent.self)
+
+ return { context in
+ let availableSize = context.availableSize
+ let component = context.component
+
+ let sideInset: CGFloat = 16.0
+ let textSideInset: CGFloat = 24.0
+
+ let textColor = component.textColor
+ let textFont = Font.regular(17.0)
+ let boldTextFont = Font.semibold(17.0)
+
+ let content = children["main"].update(
+ component: component.content,
+ environment: {
+ context.environment[ChildEnvironment.self]
+ },
+ availableSize: CGSize(width: availableSize.width, height: availableSize.width),
+ transition: context.transition
+ )
+
+ let title = title.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: component.title,
+ font: boldTextFont,
+ textColor: component.textColor,
+ paragraphAlignment: .center
+ )),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 1
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
+ transition: .immediate
+ )
+
+ let markdownAttributes = MarkdownAttributes(
+ body: MarkdownAttributeSet(font: textFont, textColor: textColor),
+ bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
+ link: MarkdownAttributeSet(font: textFont, textColor: textColor),
+ linkAttribute: { _ in
+ return nil
+ }
+ )
+ let text = text.update(
+ component: MultilineTextComponent(
+ text: .markdown(text: component.text, attributes: markdownAttributes),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.0
+ ),
+ availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
+ transition: .immediate
+ )
+
+ context.add(title
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0))
+ )
+ context.add(text
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0))
+ )
+ context.add(content
+ .position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0))
+ )
+
+ return availableSize
+ }
+ }
+}
+
+private final class DemoSheetContent: CombinedComponent {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let action: () -> Void
+ let dismiss: () -> Void
+
+ init(
+ context: AccountContext,
+ action: @escaping () -> Void,
+ dismiss: @escaping () -> Void
+ ) {
+ self.context = context
+ self.action = action
+ self.dismiss = dismiss
+ }
+
+ static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ return true
+ }
+
+ final class State: ComponentState {
+ private let context: AccountContext
+
+ private var disposable: Disposable?
+ private(set) var promoConfiguration: PremiumPromoConfiguration?
+
+ fileprivate let playButtonAnimation = ActionSlot()
+ private var didPlayAnimation = false
+
+ init(context: AccountContext) {
+ self.context = context
+
+ super.init()
+
+ self.disposable = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.PremiumPromo())
+ |> deliverOnMainQueue).start(next: { [weak self] promoConfiguration in
+ guard let self else {
+ return
+ }
+ self.promoConfiguration = promoConfiguration
+ self.updated(transition: .immediate)
+ })
+ }
+
+ deinit {
+ self.disposable?.dispose()
+ }
+
+ func playAnimationIfNeeded() {
+ if !self.didPlayAnimation {
+ self.didPlayAnimation = true
+ self.playButtonAnimation.invoke(Void())
+ }
+ }
+ }
+
+ func makeState() -> State {
+ return State(context: self.context)
+ }
+
+ static var body: Body {
+ let closeButton = Child(GlassBarButtonComponent.self)
+ let background = Child(PremiumGradientBackgroundComponent.self)
+ let demo = Child(PhoneDemoComponent.self)
+ let scroll = Child(ScrollComponent.self)
+ let bottomEdgeEffect = Child(EdgeEffectComponent.self)
+ let button = Child(ButtonComponent.self)
+
+ return { context in
+ let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
+ let component = context.component
+ let theme = environment.theme
+ let strings = environment.strings
+
+ let state = context.state
+
+ var contentSize = CGSize(width: context.availableSize.width, height: context.availableSize.width)
+
+ let remainingHeight = 365.0
+ let scroll = scroll.update(
+ component: ScrollComponent(
+ content: AnyComponent(
+ GiftDemoListComponent(
+ context: context.component.context,
+ theme: environment.theme
+ )
+ ),
+ contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 80.0, right: 0.0),
+ contentOffsetUpdated: { _, _ in },
+ contentOffsetWillCommit: { _ in }
+ ),
+ availableSize: CGSize(width: context.availableSize.width, height: remainingHeight),
+ transition: context.transition
+ )
+ context.add(scroll
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + scroll.size.height / 2.0))
+ )
+
+ let background = background.update(
+ component: PremiumGradientBackgroundComponent(colors: [
+ UIColor(rgb: 0x0077ff),
+ UIColor(rgb: 0x6b93ff),
+ UIColor(rgb: 0x8878ff),
+ UIColor(rgb: 0xe46ace)
+ ]),
+ availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
+ transition: .immediate
+ )
+ context.add(background
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
+ )
+
+ let demo = demo.update(
+ component: PhoneDemoComponent(
+ context: component.context,
+ position: .top,
+ model: .island,
+ videoFile: state.promoConfiguration?.videos["gifts"],
+ decoration: .badgeStars
+ ),
+ environment: { DemoPageEnvironment(isDisplaying: true, isCentral: true, position: 0.0) },
+ availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width),
+ transition: context.transition
+ )
+ context.add(demo
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: demo.size.height / 2.0))
+ )
+
+ let closeButton = closeButton.update(
+ component: GlassBarButtonComponent(
+ size: CGSize(width: 40.0, height: 40.0),
+ backgroundColor: UIColor(rgb: 0x7f76f4),
+ isDark: false,
+ state: .tintedGlass,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: .white
+ )
+ )),
+ action: { _ in
+ component.dismiss()
+ }
+ ),
+ availableSize: CGSize(width: 40.0, height: 40.0),
+ transition: .immediate
+ )
+ context.add(closeButton
+ .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
+ )
+
+ contentSize.height += remainingHeight
+
+ let bottomEdgeEffectHeight = 108.0
+ let bottomEdgeEffect = bottomEdgeEffect.update(
+ component: EdgeEffectComponent(
+ color: .clear,
+ blur: true,
+ alpha: 1.0,
+ size: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
+ edge: .bottom,
+ edgeSize: bottomEdgeEffectHeight
+ ),
+ availableSize: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight),
+ transition: context.transition
+ )
+ context.add(bottomEdgeEffect
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - bottomEdgeEffect.size.height / 2.0))
+ )
+
+ var buttonTitle: [AnyComponentWithIdentity] = []
+ buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent(
+ content: LottieComponent.AppBundleContent(name: "anim_ok"),
+ color: theme.list.itemCheckColors.foregroundColor,
+ startingPosition: .begin,
+ size: CGSize(width: 28.0, height: 28.0),
+ playOnce: state.playButtonAnimation
+ ))))
+ buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
+ text: strings.Gift_Demo_Understood,
+ badge: 0,
+ textColor: theme.list.itemCheckColors.foregroundColor,
+ badgeBackground: theme.list.itemCheckColors.foregroundColor,
+ badgeForeground: theme.list.itemCheckColors.fillColor
+ ))))
+ let button = button.update(
+ component: ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: theme.list.itemCheckColors.fillColor,
+ foreground: theme.list.itemCheckColors.foregroundColor,
+ pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
+ cornerRadius: 10.0,
+ ),
+ content: AnyComponentWithIdentity(
+ id: AnyHashable(0),
+ component: AnyComponent(HStack(buttonTitle, spacing: 2.0))
+ ),
+ action: {
+ component.dismiss()
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
+ transition: .immediate
+ )
+ context.add(button
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - button.size.height / 2.0 - 30.0))
+ )
+
+ state.playAnimationIfNeeded()
+
+ return contentSize
+ }
+ }
+}
+
+
+private final class DemoSheetComponent: CombinedComponent {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let action: () -> Void
+
+ init(context: AccountContext, action: @escaping () -> Void) {
+ self.context = context
+ self.action = action
+ }
+
+ static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ return true
+ }
+
+ static var body: Body {
+ let sheet = Child(SheetComponent<(EnvironmentType)>.self)
+ let animateOut = StoredActionSlot(Action.self)
+
+ return { context in
+ let environment = context.environment[EnvironmentType.self]
+
+ let controller = environment.controller
+
+ let sheet = sheet.update(
+ component: SheetComponent(
+ content: AnyComponent(DemoSheetContent(
+ context: context.component.context,
+ action: context.component.action,
+ dismiss: {
+ animateOut.invoke(Action { _ in
+ if let controller = controller() {
+ controller.dismiss(completion: nil)
+ }
+ })
+ }
+ )),
+ style: .glass,
+ backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
+ followContentSizeChanges: true,
+ animateOut: animateOut
+ ),
+ environment: {
+ environment
+ SheetComponentEnvironment(
+ isDisplaying: environment.value.isVisible,
+ isCentered: environment.metrics.widthClass == .regular,
+ hasInputHeight: !environment.inputHeight.isZero,
+ regularMetricsSize: nil,
+ dismiss: { animated in
+ if animated {
+ animateOut.invoke(Action { _ in
+ if let controller = controller() {
+ controller.dismiss(completion: nil)
+ }
+ })
+ } else {
+ if let controller = controller() {
+ controller.dismiss(completion: nil)
+ }
+ }
+ }
+ )
+ },
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+
+ context.add(sheet
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
+ )
+
+ return context.availableSize
+ }
+ }
+}
+
+public class GiftDemoScreen: ViewControllerComponentContainer {
+ public init(context: AccountContext, action: @escaping () -> Void = {}) {
+ super.init(context: context, component: DemoSheetComponent(context: context, action: action), navigationBarAppearance: .none, theme: .default)
+
+ self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
+
+ self.navigationPresentation = .flatModal
+ }
+
+ required public init(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.view.disablesInteractiveModalDismiss = true
+ }
+
+ public func dismissAnimated() {
+ if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View {
+ view.dismissAnimated()
+ }
+ }
+}
+
+private final class GiftDemoListComponent: CombinedComponent {
+ typealias EnvironmentType = (Empty, ScrollChildEnvironment)
+
+ let context: AccountContext
+ let theme: PresentationTheme
+
+ init(context: AccountContext, theme: PresentationTheme) {
+ self.context = context
+ self.theme = theme
+ }
+
+ static func ==(lhs: GiftDemoListComponent, rhs: GiftDemoListComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ return true
+ }
+
+ static var body: Body {
+ let title = Child(MultilineTextComponent.self)
+ let description = Child(BalancedTextComponent.self)
+ let list = Child(List.self)
+
+ return { context in
+ let theme = context.component.theme
+ let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
+
+ let titleColor = theme.list.itemPrimaryTextColor
+ let textColor = theme.list.itemSecondaryTextColor
+ let iconColor = theme.list.itemAccentColor
+
+ var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
+
+ let title = title.update(
+ component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Demo_Title, font: Font.bold(25.0), textColor: titleColor))),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+ context.add(title
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height * 0.5))
+ )
+ contentSize.height += title.size.height
+ contentSize.height += 9.0
+
+ let description = description.update(
+ component: BalancedTextComponent(
+ text: .plain(NSAttributedString(string: strings.Gift_Demo_Description, font: Font.regular(15.0), textColor: textColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.2
+ ),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+ context.add(description
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5))
+ )
+ contentSize.height += description.size.height
+ contentSize.height += 26.0
+
+ var items: [AnyComponentWithIdentity] = []
+ items.append(
+ AnyComponentWithIdentity(
+ id: "unique",
+ component: AnyComponent(InfoParagraphComponent(
+ title: strings.Gift_Demo_Unique_Title,
+ titleColor: titleColor,
+ text: strings.Gift_Demo_Unique_Text,
+ textColor: textColor,
+ accentColor: iconColor,
+ iconName: "Premium/Collectible/Unique",
+ iconColor: iconColor
+ ))
+ )
+ )
+ items.append(
+ AnyComponentWithIdentity(
+ id: "tradable",
+ component: AnyComponent(InfoParagraphComponent(
+ title: strings.Gift_Demo_Tradable_Title,
+ titleColor: titleColor,
+ text: strings.Gift_Demo_Tradable_Text,
+ textColor: textColor,
+ accentColor: iconColor,
+ iconName: "Premium/Collectible/Transferable",
+ iconColor: iconColor
+ ))
+ )
+ )
+ items.append(
+ AnyComponentWithIdentity(
+ id: "wearable",
+ component: AnyComponent(InfoParagraphComponent(
+ title: strings.Gift_Demo_Wearable_Title,
+ titleColor: titleColor,
+ text: strings.Gift_Demo_Wearable_Text,
+ textColor: textColor,
+ accentColor: iconColor,
+ iconName: "Premium/Collectible/Tradable",
+ iconColor: iconColor
+ ))
+ )
+ )
+
+ let list = list.update(
+ component: List(items),
+ availableSize: CGSize(width: context.availableSize.width - 32.0 * 2.0, height: 10000.0),
+ transition: context.transition
+ )
+ context.add(list
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
+ )
+ contentSize.height += list.size.height
+ contentSize.height += 88.0
+
+ return CGSize(width: context.availableSize.width, height: contentSize.height)
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift
index 6fc7466c93..55b04a7ee3 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift
@@ -29,7 +29,8 @@ public final class GiftItemComponent: Component {
case premium(months: Int32, price: String)
case starGift(gift: StarGift.Gift, price: String)
case uniqueGift(gift: StarGift.UniqueGift, price: String?)
- case auction(gift: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32)
+ case auction(gift: StarGift.Gift, endTime: Int32)
+ case preview(attributes: [StarGift.UniqueGift.Attribute], rarity: Int32)
}
public struct Ribbon: Equatable {
@@ -144,6 +145,7 @@ public final class GiftItemComponent: Component {
case buttonIcon
case tableIcon
case header
+ case upgradePreview
}
let context: AccountContext
@@ -165,6 +167,8 @@ public final class GiftItemComponent: Component {
let isPinned: Bool
let isEditing: Bool
let isDateLocked: Bool
+ let isPlaceholder: Bool
+ let animateChanges: Bool
let mode: Mode
let action: (() -> Void)?
let contextAction: ((UIView, ContextGesture) -> Void)?
@@ -189,6 +193,8 @@ public final class GiftItemComponent: Component {
isPinned: Bool = false,
isEditing: Bool = false,
isDateLocked: Bool = false,
+ isPlaceholder: Bool = false,
+ animateChanges: Bool = false,
mode: Mode = .generic,
action: (() -> Void)? = nil,
contextAction: ((UIView, ContextGesture) -> Void)? = nil
@@ -212,6 +218,8 @@ public final class GiftItemComponent: Component {
self.isPinned = isPinned
self.isEditing = isEditing
self.isDateLocked = isDateLocked
+ self.isPlaceholder = isPlaceholder
+ self.animateChanges = animateChanges
self.mode = mode
self.action = action
self.contextAction = contextAction
@@ -275,6 +283,12 @@ public final class GiftItemComponent: Component {
if lhs.isDateLocked != rhs.isDateLocked {
return false
}
+ if lhs.isPlaceholder != rhs.isPlaceholder {
+ return false
+ }
+ if lhs.animateChanges != rhs.animateChanges {
+ return false
+ }
if lhs.mode != rhs.mode {
return false
}
@@ -421,6 +435,14 @@ public final class GiftItemComponent: Component {
size = availableSize
iconSize = CGSize(width: 106.0, height: 106.0)
cornerRadius = 16.0
+ case .upgradePreview:
+ size = availableSize
+ if case let .preview(attributes, _) = component.subject, attributes.count == 2 {
+ iconSize = CGSize(width: 60.0, height: 60.0)
+ } else {
+ iconSize = CGSize(width: 72.0, height: 72.0)
+ }
+ cornerRadius = 16.0
}
var backgroundSize = size
if case .grid = component.mode {
@@ -523,12 +545,18 @@ public final class GiftItemComponent: Component {
} else {
emoji = nil
}
- case let .auction(gift, centerColor, edgeColor, endTime):
+ case let .auction(gift, endTime):
animationOffset = 16.0
explicitAnimationOffset = -16.0
animationFile = gift.file
- backgroundColor = edgeColor
- secondBackgroundColor = centerColor
+
+ if let background = gift.background {
+ backgroundColor = UIColor(rgb: UInt32(bitPattern: background.edgeColor))
+ secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: background.centerColor))
+ } else {
+ backgroundColor = UIColor.black
+ secondBackgroundColor = UIColor.black
+ }
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
@@ -537,8 +565,14 @@ public final class GiftItemComponent: Component {
)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
- let endTimeout = max(0, endTime - currentTime)
+ let endTimeout: Int32
+ if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate {
+ endTimeout = max(0, auctionStartDate - currentTime)
+ animatedBadgeItems.append(AnimatedTextComponent.Item(id: "starts", content: .text(component.strings.Chat_Auction_StartsIn)))
+ } else {
+ endTimeout = max(0, endTime - currentTime)
+ }
if endTimeout > 0 {
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
@@ -565,6 +599,47 @@ public final class GiftItemComponent: Component {
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
}
+ case let .preview(attributes, _):
+ animationOffset = 16.0
+ if component.mode != .tableIcon {
+ explicitAnimationOffset = -4.0
+ }
+ for attribute in attributes {
+ switch attribute {
+ case let .model(_, file, _):
+ animationFile = file
+ if !self.fetchedFiles.contains(file.fileId.id) {
+ self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
+ self.fetchedFiles.insert(file.fileId.id)
+ }
+ case let .pattern(_, file, _):
+ patternFile = file
+ files[file.fileId.id] = file
+ case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _):
+ backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue))
+ secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue))
+ patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue))
+ if let backgroundColor {
+ placeholderColor = backgroundColor
+ }
+ default:
+ break
+ }
+ }
+
+ if animationFile == nil, let patternFile {
+ animationFile = patternFile
+ }
+
+ if let animationFile {
+ emoji = ChatTextInputTextCustomEmojiAttribute(
+ interactivelySelectedFromPackId: nil,
+ fileId: animationFile.fileId.id,
+ file: animationFile
+ )
+ } else {
+ emoji = nil
+ }
}
if [.buttonIcon, .tableIcon].contains(component.mode) {
@@ -575,12 +650,22 @@ public final class GiftItemComponent: Component {
}
var animationTransition = transition
+ var animateBackgroundChange = false
if self.animationLayer == nil || self.animationFile?.fileId != animationFile?.fileId, let emoji {
animationTransition = .immediate
self.animationFile = animationFile
+ var animateAppearance = false
if let animationLayer = self.animationLayer {
self.animationLayer = nil
- animationLayer.removeFromSuperlayer()
+ if component.animateChanges {
+ animateAppearance = true
+ animateBackgroundChange = true
+ animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
+ animationLayer.removeFromSuperlayer()
+ })
+ } else {
+ animationLayer.removeFromSuperlayer()
+ }
}
let animationLayer = InlineStickerItemLayer(
context: .account(component.context),
@@ -603,6 +688,9 @@ public final class GiftItemComponent: Component {
} else {
self.layer.insertSublayer(animationLayer, above: self.backgroundLayer)
}
+ if animateAppearance {
+ animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
+ }
}
let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.mode == .generic ? animationOffset : (floorToScreenPixels((size.height - iconSize.height) / 2.0) + explicitAnimationOffset)), size: iconSize)
@@ -611,6 +699,11 @@ public final class GiftItemComponent: Component {
}
if let backgroundColor {
+ if let backgroundView = self.patternView.view as? PeerInfoCoverComponent.View {
+ if animateBackgroundChange {
+ backgroundView.animateTransition(background: true, bounce: false)
+ }
+ }
let _ = self.patternView.update(
transition: .immediate,
component: AnyComponent(PeerInfoCoverComponent(
@@ -627,7 +720,7 @@ public final class GiftItemComponent: Component {
environment: {},
containerSize: backgroundSize
)
- if let backgroundView = self.patternView.view {
+ if let backgroundView = self.patternView.view as? PeerInfoCoverComponent.View {
if backgroundView.superview == nil {
backgroundView.layer.cornerRadius = cornerRadius
if #available(iOS 13.0, *) {
@@ -640,7 +733,62 @@ public final class GiftItemComponent: Component {
}
}
- if case .preview = component.mode {
+ if case .upgradePreview = component.mode, case let .preview(attributes, rarity) = component.subject {
+ let isColored = attributes.count > 1
+ if let title = component.title {
+ let titleSize = self.title.update(
+ transition: transition,
+ component: AnyComponent(
+ MultilineTextComponent(
+ text: .plain(NSAttributedString(string: title, font: Font.medium(13.0), textColor: isColored ? .white : component.theme.list.itemPrimaryTextColor)),
+ horizontalAlignment: .center
+ )
+ ),
+ environment: {},
+ containerSize: availableSize
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: size.height - 27.0), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.addSubview(titleView)
+ }
+ transition.setFrame(view: titleView, frame: titleFrame)
+ }
+ }
+
+ func formatPercentage(_ value: Float) -> String {
+ return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%"
+ }
+ let percentage = Float(rarity) * 0.1
+
+ let badgeTextSize = self.badgeText.update(
+ transition: .spring(duration: 0.2),
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: formatPercentage(percentage), font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: isColored ? .white : component.theme.list.itemSecondaryTextColor)))
+ ),
+ environment: {},
+ containerSize: availableSize
+ )
+
+ let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 12.0, height: 18.0)
+ let _ = self.badgeBackground.update(
+ transition: .spring(duration: 0.2),
+ component: AnyComponent(
+ RoundedRectangle(color: isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06), cornerRadius: 9.0)
+ ),
+ environment: {},
+ containerSize: badgeBackgroundSize
+ )
+
+ if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view {
+ if badgeBackgroundView.superview == nil {
+ self.addSubview(badgeBackgroundView)
+ self.addSubview(badgeTextView)
+ }
+ badgeTextView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - badgeTextSize.width, y: 9.0), size: badgeTextSize)
+ badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize)
+ }
+ } else if case .preview = component.mode {
if let title = component.title {
let titleSize = self.title.update(
transition: transition,
@@ -793,13 +941,15 @@ public final class GiftItemComponent: Component {
case .auction:
buttonColor = .clear
price = ""
- break
+ case .preview:
+ buttonColor = .clear
+ price = ""
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(
- ButtonContentComponent(
+ StarsButtonContentComponent(
context: component.context,
text: price,
color: buttonColor,
@@ -1189,9 +1339,19 @@ public final class GiftItemComponent: Component {
}
switch component.mode {
- case .generic, .grid:
+ case .generic, .grid, .upgradePreview:
let lineWidth: CGFloat = 2.0
- let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0)
+ let selectionFrame = backgroundFrame.insetBy(dx: 2.0, dy: 2.0)
+
+ var cornerRadius: CGFloat = 6.0
+ if case .upgradePreview = component.mode {
+ cornerRadius = 13.0
+ }
+
+ var selectionColor = UIColor.white
+ if case .upgradePreview = component.mode, case let .preview(attributes, _) = component.subject, attributes.count == 1 {
+ selectionColor = component.theme.list.itemAccentColor
+ }
if component.isSelected {
let selectionLayer: SimpleShapeLayer
@@ -1207,13 +1367,13 @@ public final class GiftItemComponent: Component {
}
selectionLayer.fillColor = UIColor.clear.cgColor
- selectionLayer.strokeColor = UIColor.white.cgColor
+ selectionLayer.strokeColor = selectionColor.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
- selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
+ selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
if !transition.animation.isImmediate {
- let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
+ let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
}
@@ -1222,7 +1382,7 @@ public final class GiftItemComponent: Component {
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
- let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
+ let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
selectionLayer.removeFromSuperlayer()
@@ -1340,28 +1500,34 @@ public final class GiftItemComponent: Component {
}
}
-private final class ButtonContentComponent: Component {
+public final class StarsButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
let tinted: Bool
let starsColor: UIColor?
+ let font: UIFont
+ let height: CGFloat
public init(
context: AccountContext,
text: String,
color: UIColor,
tinted: Bool = false,
- starsColor: UIColor? = nil
+ starsColor: UIColor? = nil,
+ font: UIFont = Font.semibold(11.0),
+ height: CGFloat = 30.0
) {
self.context = context
self.text = text
self.color = color
self.tinted = tinted
self.starsColor = starsColor
+ self.font = font
+ self.height = height
}
- public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
+ public static func ==(lhs: StarsButtonContentComponent, rhs: StarsButtonContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
@@ -1377,11 +1543,17 @@ private final class ButtonContentComponent: Component {
if lhs.starsColor != rhs.starsColor {
return false
}
+ if lhs.font != rhs.font {
+ return false
+ }
+ if lhs.height != rhs.height {
+ return false
+ }
return true
}
public final class View: UIView {
- private var component: ButtonContentComponent?
+ private var component: StarsButtonContentComponent?
private weak var componentState: EmptyComponentState?
private let backgroundLayer = SimpleLayer()
@@ -1400,7 +1572,7 @@ private final class ButtonContentComponent: Component {
fatalError("init(coder:) has not been implemented")
}
- func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ func update(component: StarsButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
@@ -1409,7 +1581,7 @@ private final class ButtonContentComponent: Component {
textColor = .white
}
- let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: textColor)
+ let attributedText = NSMutableAttributedString(string: component.text, font: component.font, textColor: textColor)
let range = (attributedText.string as NSString).range(of: "#")
if range.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: component.tinted)), range: range)
@@ -1435,7 +1607,7 @@ private final class ButtonContentComponent: Component {
)
let padding: CGFloat = 9.0
- let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0)
+ let size = CGSize(width: titleSize.width + padding * 2.0, height: component.height)
if let starsColor = component.starsColor {
let starsLayer: StarsButtonEffectLayer
@@ -1446,8 +1618,10 @@ private final class ButtonContentComponent: Component {
self.layer.addSublayer(starsLayer)
self.starsLayer = starsLayer
}
+ starsLayer.masksToBounds = true
starsLayer.frame = CGRect(origin: .zero, size: size)
starsLayer.update(color: starsColor, size: size)
+ starsLayer.cornerRadius = size.height * 0.5
} else {
self.starsLayer?.removeFromSuperlayer()
self.starsLayer = nil
@@ -1472,7 +1646,7 @@ private final class ButtonContentComponent: Component {
}
}
- self.backgroundLayer.backgroundColor = backgroundColor.cgColor
+ transition.setBackgroundColor(layer: self.backgroundLayer, color: backgroundColor)
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
self.backgroundLayer.cornerRadius = size.height / 2.0
diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift
index 7de15d9924..7176042275 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift
@@ -151,17 +151,17 @@ final class GiftOptionsScreenComponent: Component {
private var loadingGiftId: Int64?
- private var _effectiveStarGifts: ([StarGift], StarsFilter)?
+ private var _effectiveStarGifts: ([StarGift], StarsFilter, Int)?
private var effectiveStarGifts: [StarGift]? {
get {
- if let (currentGifts, currentFilter) = self._effectiveStarGifts, currentFilter == self.starsFilter && currentFilter != .transfer {
+ if let (currentGifts, currentFilter, currentVersion) = self._effectiveStarGifts, currentFilter == self.starsFilter && currentFilter != .transfer && currentVersion == self.state?.starGiftsVersion {
return currentGifts
- } else if let allGifts = self.state?.starGifts {
+ } else if let state = self.state, let allGifts = state.starGifts {
if case .transfer = self.starsFilter {
let filteredGifts: [StarGift] = self.state?.transferStarGifts?.map { gift in
return gift.gift
} ?? []
- self._effectiveStarGifts = (filteredGifts, self.starsFilter)
+ self._effectiveStarGifts = (filteredGifts, self.starsFilter, state.starGiftsVersion)
return filteredGifts
} else {
var sortedGifts = allGifts
@@ -216,7 +216,7 @@ final class GiftOptionsScreenComponent: Component {
}
return false
}
- self._effectiveStarGifts = (filteredGifts, self.starsFilter)
+ self._effectiveStarGifts = (filteredGifts, self.starsFilter, state.starGiftsVersion)
return filteredGifts
}
} else {
@@ -416,15 +416,31 @@ final class GiftOptionsScreenComponent: Component {
let giftController = component.context.sharedContext.makeGiftAuctionViewScreen(
context: component.context,
auctionContext: auctionContext,
- completion: { [weak mainController] acquiredGifts in
- let controller = GiftSetupScreen(
- context: context,
- peerId: component.peerId,
- subject: .starGift(gift, nil),
- auctionAcquiredGifts: acquiredGifts,
- completion: nil
- )
- mainController?.push(controller)
+ completion: { [weak mainController] acquiredGifts, upgradeAttributes in
+ if component.peerId == context.account.peerId, let upgradeAttributes, let navigationController = mainController?.navigationController as? NavigationController {
+ let controller = context.sharedContext.makeGiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, acquiredGifts: acquiredGifts, attributes: upgradeAttributes, completion: {
+ let controller = context.sharedContext.makeGiftAuctionBidScreen(
+ context: context,
+ toPeerId: context.account.peerId,
+ text: "",
+ entities: [],
+ hideName: true,
+ auctionContext: auctionContext,
+ acquiredGifts: acquiredGifts
+ )
+ navigationController.pushViewController(controller)
+ })
+ mainController?.push(controller)
+ } else {
+ let controller = GiftSetupScreen(
+ context: context,
+ peerId: component.peerId,
+ subject: .starGift(gift, nil),
+ auctionAcquiredGifts: acquiredGifts,
+ completion: nil
+ )
+ mainController?.push(controller)
+ }
}
)
mainController?.push(giftController)
@@ -585,6 +601,7 @@ final class GiftOptionsScreenComponent: Component {
self.starsItems[itemId] = visibleItem
}
+ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var ribbon: GiftItemComponent.Ribbon?
var outline: GiftItemComponent.Outline?
var isSoldOut = false
@@ -608,7 +625,11 @@ final class GiftOptionsScreenComponent: Component {
let text: String
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
if gift.flags.contains(.isAuction) {
- text = environment.strings.Gift_Options_Gift_Auction
+ if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate {
+ text = environment.strings.Gift_Options_Gift_Soon
+ } else {
+ text = environment.strings.Gift_Options_Gift_Auction
+ }
ribbonColor = .orange
outline = .orange
} else if let perUserLimit = gift.perUserLimit, component.peerId.namespace != Namespaces.Peer.CloudChannel {
@@ -624,7 +645,11 @@ final class GiftOptionsScreenComponent: Component {
if !isSoldOut && gift.flags.contains(.requiresPremium) {
let text: String
if gift.flags.contains(.isAuction) {
- text = environment.strings.Gift_Options_Gift_Auction
+ if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate {
+ text = environment.strings.Gift_Options_Gift_Soon
+ } else {
+ text = environment.strings.Gift_Options_Gift_Auction
+ }
} else if component.context.isPremium, let perUserLimit = gift.perUserLimit {
text = environment.strings.Gift_Options_Gift_Premium_Left(perUserLimit.remains)
} else {
@@ -661,11 +686,13 @@ final class GiftOptionsScreenComponent: Component {
case let .generic(gift):
if gift.flags.contains(.isAuction) {
var action = environment.strings.Gift_Options_Gift_JoinAuction
- if gift.availability?.remains == 0 {
+ if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate {
+ action = environment.strings.Gift_Options_Gift_ViewAuction
+ } else if gift.availability?.remains == 0 {
action = environment.strings.Gift_Options_Gift_ViewAuction
}
subject = .starGift(gift: gift, price: action)
- } else if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars {
+ } else if let availability = gift.availability, availability.remains == 0, availability.resale > 0, let minResaleStars = availability.minResaleStars {
let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator)
if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxStarsAmount || availability.resale == 1 {
subject = .starGift(gift: gift, price: "# \(priceString)")
@@ -1718,6 +1745,7 @@ final class GiftOptionsScreenComponent: Component {
fileprivate var disallowedGifts: TelegramDisallowedGifts?
fileprivate var premiumProducts: [PremiumGiftProduct]?
fileprivate var starGifts: [StarGift]?
+ fileprivate var starGiftsVersion: Int = 0
fileprivate let starGiftsContext: ProfileGiftsContext
fileprivate var transferStarGifts: [ProfileGiftsContext.State.StarGift]?
@@ -1739,7 +1767,7 @@ final class GiftOptionsScreenComponent: Component {
} else {
availableProducts = .single([])
}
-
+
self.disposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(
@@ -1861,6 +1889,10 @@ final class GiftOptionsScreenComponent: Component {
return true
}
}
+
+ if self.starGifts != filteredStarGifts {
+ self.starGiftsVersion += 1
+ }
self.starGifts = filteredStarGifts
self.updated()
diff --git a/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD
new file mode 100644
index 0000000000..71148481be
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD
@@ -0,0 +1,29 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "GiftRemainingCountComponent",
+ module_name = "GiftRemainingCountComponent",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AsyncDisplayKit",
+ "//submodules/Display",
+ "//submodules/Postbox",
+ "//submodules/TelegramCore",
+ "//submodules/SSignalKit/SwiftSignalKit",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ComponentDisplayAdapters",
+ "//submodules/TelegramPresentationData",
+ "//submodules/AccountContext",
+ "//submodules/AppBundle",
+ "//submodules/TelegramUI/Components/GlassBackgroundComponent",
+ "//submodules/Components/MultilineTextComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift
similarity index 97%
rename from submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift
rename to submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift
index 37ceb3f1ee..eee3bd66cf 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift
@@ -6,15 +6,11 @@ import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
-import PresentationDataUtils
import ComponentFlow
import MultilineTextComponent
-import Markdown
-import TextFormat
-import RoundedRectWithTailPath
import GlassBackgroundComponent
-public class RemainingCountComponent: Component {
+public class GiftRemainingCountComponent: Component {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
private let inactiveTitle: String
@@ -62,7 +58,7 @@ public class RemainingCountComponent: Component {
self.groupingSeparator = groupingSeparator
}
- public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
+ public static func ==(lhs: GiftRemainingCountComponent, rhs: GiftRemainingCountComponent) -> Bool {
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
@@ -109,7 +105,7 @@ public class RemainingCountComponent: Component {
}
public final class View: UIView {
- private var component: RemainingCountComponent?
+ private var component: GiftRemainingCountComponent?
private let container: UIView
private let inactiveBackground: SimpleLayer
@@ -162,7 +158,7 @@ public class RemainingCountComponent: Component {
}
var previousAvailableSize: CGSize?
- func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
+ func update(component: GiftRemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD
index cce609e7d9..7e874b3f9b 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD
+++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD
@@ -46,11 +46,12 @@ swift_library(
"//submodules/InAppPurchaseManager",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/ProgressNavigationButtonNode",
- "//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/ConfettiEffect",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
+ "//submodules/TelegramUI/Components/MessageInputPanelComponent",
+ "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift
index 5ae83ff711..0175318100 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift
@@ -245,7 +245,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
case let .starGift(gift):
media = [
TelegramMediaAction(
- action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil)
+ action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil, number: nil)
)
]
}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift
index 5adbd507c2..3df1b80480 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift
@@ -32,13 +32,13 @@ import InAppPurchaseManager
import BlurredBackgroundComponent
import ProgressNavigationButtonNode
import Markdown
-import GiftViewScreen
import UndoUI
import ConfettiEffect
import EdgeEffect
import AnimatedTextComponent
import GlassBarButtonComponent
import MessageInputPanelComponent
+import GiftRemainingCountComponent
private final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -1596,10 +1596,7 @@ private final class GiftSetupScreenComponent: Component {
guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else {
return
}
- let previewController = GiftViewScreen(
- context: component.context,
- subject: .upgradePreview(upgradePreview.attributes, peerName)
- )
+ let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName)
controller.push(previewController)
})
}
@@ -1712,7 +1709,7 @@ private final class GiftSetupScreenComponent: Component {
var remains: Int32 = availability.remains
if let auctionState = self.giftAuctionState {
switch auctionState.auctionState {
- case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _):
+ case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
remains = giftsLeft
case .finished:
remains = 0
@@ -1723,14 +1720,14 @@ private final class GiftSetupScreenComponent: Component {
let sold = total - remains
let remainingCountSize = self.remainingCount.update(
transition: transition,
- component: AnyComponent(RemainingCountComponent(
+ component: AnyComponent(GiftRemainingCountComponent(
inactiveColor: theme.list.itemBlocksBackgroundColor,
activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)],
inactiveTitle: environment.strings.Gift_Send_Remains(remains),
inactiveValue: "",
inactiveTitleColor: theme.list.itemSecondaryTextColor,
activeTitle: "",
- activeValue: environment.strings.Gift_Send_Sold(sold),
+ activeValue: sold > 0 ? environment.strings.Gift_Send_Sold(sold) : "",
activeTitleColor: .white,
badgeText: "",
badgePosition: position,
@@ -1833,22 +1830,30 @@ private final class GiftSetupScreenComponent: Component {
var buttonTitleItems: [AnyComponentWithIdentity] = []
if let _ = self.giftAuction {
- let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Gift_Setup_PlaceBid, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
- buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent(
- MultilineTextComponent(text: .plain(buttonAttributedString))
- )))
+ var isUpcoming = false
if let giftAuctionState = self.giftAuctionState {
switch giftAuctionState.auctionState {
- case let .ongoing(_, _, endTime, _, _, _, _, _, _, _):
+ case let .ongoing(_, startTime, endTime, _, _, _, _, _, _, _, _, _):
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
-
- let endTimeout = max(0, endTime - currentTime)
+ let endTimeout: Int32
+ if currentTime < startTime {
+ endTimeout = max(0, startTime - currentTime)
+ isUpcoming = true
+ } else {
+ endTimeout = max(0, endTime - currentTime)
+ }
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
- let rawString = hours > 0 ? environment.strings.Gift_Auction_TimeLeftHours : environment.strings.Gift_Auction_TimeLeftMinutes
+ let rawString: String
+ if isUpcoming {
+ rawString = hours > 0 ? environment.strings.Gift_Auction_StartsInHours : environment.strings.Gift_Auction_StartsInMinutes
+ } else {
+ rawString = hours > 0 ? environment.strings.Gift_Auction_TimeLeftHours : environment.strings.Gift_Auction_TimeLeftMinutes
+ }
+
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
@@ -1888,6 +1893,10 @@ private final class GiftSetupScreenComponent: Component {
buttonIsEnabled = false
}
}
+ let buttonAttributedString = NSMutableAttributedString(string: isUpcoming ? environment.strings.Gift_Auction_EarlyBid : environment.strings.Gift_Setup_PlaceBid, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
+ buttonTitleItems.insert(AnyComponentWithIdentity(id: "bid", component: AnyComponent(
+ MultilineTextComponent(text: .plain(buttonAttributedString))
+ )), at: 0)
} else {
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 {
diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift
index fa59cfe9e3..f8fcc34ce1 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift
@@ -167,8 +167,6 @@ final class GiftStoreScreenComponent: Component {
return
}
- //, self.state?.starGiftsState?.dataState != .loading
-
let availableWidth = self.scrollView.bounds.width
let availableHeight = self.scrollView.bounds.height
let contentOffset = self.scrollView.contentOffset.y
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD
index 350b39f791..a5d6d5dcee 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD
@@ -61,6 +61,9 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/BotPaymentsUI",
+ "//submodules/TelegramUI/Components/SegmentControlComponent",
+ "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
+ "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift
index 49e8bd7b16..574e2a4d45 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift
@@ -328,11 +328,21 @@ private final class GiftAuctionAcquiredScreenComponent: Component {
var giftSubject: GiftItemComponent.Subject?
+ var giftTitle: String = ""
if case let .generic(gift) = component.gift {
giftSubject = .starGift(gift: gift, price: "")
+ giftTitle = gift.title ?? ""
}
if let giftSubject {
+ let titleString: String
+ if let number = gift.number {
+ let fullGiftTitle = "\(giftTitle) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))"
+ titleString = environment.strings.Gift_Acquired_GiftRound(fullGiftTitle, "\(gift.round)").string
+ } else {
+ titleString = environment.strings.Gift_Acquired_Round("\(gift.round)").string
+ }
+
items.append(.init(
id: "header",
title: nil,
@@ -351,7 +361,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component {
AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
- MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Acquired_Round("\(gift.round)").string, font: tableBoldFont, textColor: tableTextColor)))
+ MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: tableBoldFont, textColor: tableTextColor)))
)
)
], spacing: 1.0))
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift
index 69b0b52f8e..1904b87058 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift
@@ -663,11 +663,13 @@ private final class ActiveAuctionComponent: Component {
var titleText: String = ""
var subtitleText: String = ""
var subtitleTextColor = component.theme.list.itemPrimaryTextColor
- if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, currentRound, totalRound) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
+ if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount {
titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string
let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))"
- if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
+ if component.currentTime < startDate {
+ subtitleText = component.strings.Gift_ActiveAuctions_UpcomingBid
+ } else if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound {
subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string
subtitleTextColor = component.theme.list.itemDestructiveColor
} else {
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift
index 4e1c23bd62..9863ae0d7e 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift
@@ -216,6 +216,12 @@ private final class BadgeComponent: Component {
subtitleView.alpha = 1.0
}
badgeOffset.y -= 6.0 + UIScreenPixel
+
+ let subtitleBadgeWidth = subtitleSize.width + 60.0
+ if subtitleBadgeWidth > badgeWidth {
+ badgeOffset.x -= (subtitleBadgeWidth - badgeWidth) * 0.5
+ badgeWidth = subtitleBadgeWidth
+ }
} else if let subtitleView = self.subtitle.view {
subtitleView.alpha = 0.0
}
@@ -450,7 +456,7 @@ private final class PeerPlaceComponent: Component {
if let place = component.place {
placeString = presentationStringsFormattedNumber(place, component.groupingSeparator)
if place >= 100 {
- placeString = "\(placeString)+"
+ placeString = "\(compactNumericCountString(Int(place), decimalSeparator: ".", showDecimalPart: false))+"
}
} else {
placeString = "–"
@@ -606,7 +612,7 @@ private final class PeerComponent: Component {
var color = component.theme.list.itemSecondaryTextColor
switch component.status {
case .winning:
- color = component.theme.list.itemDisclosureActions.constructive.fillColor
+ color = UIColor(rgb: 0x53a939)
case .outbid, .returned:
color = component.theme.list.itemDestructiveColor
default:
@@ -1141,8 +1147,7 @@ private final class GiftAuctionBidScreenComponent: Component {
private let sliderPlus = ComponentView()
private let badge = ComponentView()
- private var liveStreamPerks: [ComponentView] = []
- private var liveStreamMessagePreview: ComponentView?
+ private var auctionStats: [ComponentView] = []
private let myGifts = ComponentView()
@@ -1818,7 +1823,7 @@ private final class GiftAuctionBidScreenComponent: Component {
}
var minBidAmount: Int64 = 100
- if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _) = auctionState.auctionState {
+ if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _, _, _) = auctionState.auctionState {
minBidAmount = auctionMinBidAmount
if let myMinBidAmount = auctionState.myState.minBidAmount {
minBidAmount = myMinBidAmount
@@ -1859,13 +1864,13 @@ private final class GiftAuctionBidScreenComponent: Component {
}
var minBidAmount: Int64 = 100
var maxBidAmount: Int64 = 50000
- if case let .ongoing(_, _, _, auctionMinBidAmount, bidLevels, _, _, _, _, _) = state.auctionState {
+ if case let .ongoing(_, _, _, auctionMinBidAmount, bidLevels, _, _, _, _, _, _, _) = state.auctionState {
minBidAmount = auctionMinBidAmount
if let firstLevel = bidLevels.first(where: { $0.position == 1 }) {
maxBidAmount = max(maxBidAmount, Int64(Double(firstLevel.amount) * 1.5))
}
}
- var currentValue = max(Int(minBidAmount), 100)
+ var currentValue = Int(minBidAmount)
var minAllowedRealValue: Int64 = minBidAmount
if let myBidAmount = state.myState.bidAmount {
if let component, let bidPeerId = state.myState.bidPeerId, bidPeerId != component.toPeerId || forceMinimum, let myMinBidAmount = state.myState.minBidAmount {
@@ -2056,7 +2061,7 @@ private final class GiftAuctionBidScreenComponent: Component {
if case .finished = auctionState?.auctionState, let controller = self.environment?.controller() {
if let navigationController = controller.navigationController as? NavigationController {
controller.dismiss()
- let auctionController = context.sharedContext.makeGiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: { _ in })
+ let auctionController = context.sharedContext.makeGiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: { _, _ in })
navigationController.pushViewController(auctionController)
}
}
@@ -2186,12 +2191,93 @@ private final class GiftAuctionBidScreenComponent: Component {
giftsPerRound = giftsPerRoundValue
}
+ var myBidTitleComponent: AnyComponent?
+ var myBidComponent: AnyComponent?
+
+ var topBidsTitleComponent: AnyComponent?
+ var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = []
+
+ var isUpcoming = false
+ let place: Int32
+ if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, startDate, _, _, bidLevels, topBidders, _, _, _, _, _, lastGiftNumber) = giftAuctionState.auctionState {
+ if currentTime < startDate {
+ isUpcoming = true
+ }
+ var myBidAmount = Int64(self.amount.realValue)
+ var myBidDate = currentTime
+ var isBiddingUp = true
+
+ if let currentAmount = giftAuctionState.myState.bidAmount, let currentDate = giftAuctionState.myState.bidDate, currentAmount >= myBidAmount {
+ myBidAmount = currentAmount
+ myBidDate = currentDate
+ isBiddingUp = false
+ }
+
+ place = giftAuctionState.getPlace(myBid: myBidAmount, myBidDate: myBidDate) ?? 1
+
+ var bidTitle: String
+ var bidTitleColor: UIColor
+ var bidStatus: PeerComponent.Status?
+
+ var giftTitle: String?
+ var giftNumber: Int32?
+ if isBiddingUp {
+ bidTitleColor = environment.theme.list.itemSecondaryTextColor
+ bidTitle = environment.strings.Gift_AuctionBid_BidPreview
+ } else if isUpcoming {
+ bidTitleColor = environment.theme.list.itemSecondaryTextColor
+ bidTitle = environment.strings.Gift_AuctionBid_UpcomingBid
+ } else if giftAuctionState.myState.isReturned {
+ bidTitle = environment.strings.Gift_AuctionBid_Outbid
+ bidTitleColor = environment.theme.list.itemDestructiveColor
+ bidStatus = .returned
+ } else if place > giftsPerRound {
+ bidTitle = environment.strings.Gift_AuctionBid_Outbid
+ bidTitleColor = environment.theme.list.itemDestructiveColor
+ bidStatus = .outbid
+ } else {
+ bidTitle = environment.strings.Gift_AuctionBid_Winning
+ bidTitleColor = UIColor(rgb: 0x53a939)
+ bidStatus = .winning
+ if case let .generic(gift) = giftAuctionState.gift {
+ giftTitle = gift.title
+ giftNumber = lastGiftNumber + place
+ }
+ }
+
+ if let peer = self.peersMap[component.context.account.peerId] {
+ myBidTitleComponent = AnyComponent(PeerHeaderComponent(color: bidTitleColor, dateTimeFormat: environment.dateTimeFormat, title: bidTitle, giftTitle: giftTitle, giftNumber: giftNumber))
+ myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBidAmount, status: bidStatus, isLast: true, action: nil))
+ }
+
+ var i: Int32 = 1
+ for peer in topBidders {
+ var bid: Int64 = 0
+ for level in bidLevels {
+ if level.position == i {
+ bid = level.amount
+ break
+ }
+ }
+ topBidsComponents.append((peer.id, AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: i, amount: bid, isLast: i == topBidders.count, action: nil))))
+ i += 1
+ }
+
+ if !topBidsComponents.isEmpty {
+ topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinnersTotal("\(giftsPerRound)").string.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))
+ }
+ } else {
+ place = 1
+ }
+
var topCutoffRealValue: Int?
- if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = giftAuctionState.auctionState {
- for bidLevel in bidLevels {
- if bidLevel.position == giftsPerRound - 1 {
- topCutoffRealValue = Int(bidLevel.amount)
- break
+ if place > giftsPerRound {
+ if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _, _, _) = giftAuctionState.auctionState {
+ for bidLevel in bidLevels {
+ if bidLevel.position == giftsPerRound - 1 {
+ topCutoffRealValue = Int(bidLevel.amount)
+ break
+ }
}
}
}
@@ -2298,220 +2384,186 @@ private final class GiftAuctionBidScreenComponent: Component {
self.badgeStars.frame = starsRect
self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.midX, y: badgeFrame.maxY - 32.0))
}
-
- var myBidTitleComponent: AnyComponent?
- var myBidComponent: AnyComponent?
-
- var topBidsTitleComponent: AnyComponent?
- var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = []
-
- let place: Int32
- if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _) = giftAuctionState.auctionState {
- var myBidAmount = Int64(self.amount.realValue)
- var myBidDate = currentTime
- var isBiddingUp = true
-
- if let currentAmount = giftAuctionState.myState.bidAmount, let currentDate = giftAuctionState.myState.bidDate, currentAmount >= myBidAmount {
- myBidAmount = currentAmount
- myBidDate = currentDate
- isBiddingUp = false
- }
-
- place = giftAuctionState.getPlace(myBid: myBidAmount, myBidDate: myBidDate) ?? 1
-
- var bidTitle: String
- var bidTitleColor: UIColor
- var bidStatus: PeerComponent.Status?
- if isBiddingUp {
- bidTitleColor = environment.theme.list.itemSecondaryTextColor
- bidTitle = environment.strings.Gift_AuctionBid_BidPreview
- } else if giftAuctionState.myState.isReturned {
- bidTitle = environment.strings.Gift_AuctionBid_Outbid
- bidTitleColor = environment.theme.list.itemDestructiveColor
- bidStatus = .returned
- } else if place > giftsPerRound {
- bidTitle = environment.strings.Gift_AuctionBid_Outbid
- bidTitleColor = environment.theme.list.itemDestructiveColor
- bidStatus = .outbid
- } else {
- bidTitle = environment.strings.Gift_AuctionBid_Winning
- bidTitleColor = environment.theme.list.itemDisclosureActions.constructive.fillColor
- bidStatus = .winning
- }
-
- if let peer = self.peersMap[component.context.account.peerId] {
- myBidTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: bidTitle.uppercased(), font: Font.medium(13.0), textColor: bidTitleColor))))
- myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBidAmount, status: bidStatus, isLast: true, action: nil))
- }
-
- var i: Int32 = 1
- for peer in topBidders {
- var bid: Int64 = 0
- for level in bidLevels {
- if level.position == i {
- bid = level.amount
- break
- }
- }
- topBidsComponents.append((peer.id, AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: i, amount: bid, isLast: i == topBidders.count, action: nil))))
- i += 1
- }
-
- if !topBidsComponents.isEmpty {
- topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinners.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))
- }
- } else {
- place = 1
- }
-
- var perks: [([AnimatedTextComponent.Item], String)] = []
+
+ var auctionStats: [([AnimatedTextComponent.Item], String)] = []
var minBidAnimatedItems: [AnimatedTextComponent.Item] = []
- var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = []
+ var untilNextRoundAnimatedItems: [AnimatedTextComponent.Item] = []
var dropsLeftAnimatedItems: [AnimatedTextComponent.Item] = []
- if let auctionState = self.giftAuctionState?.auctionState {
- if case let .ongoing(_, _, _, minBidAmount, _, _, nextDropDate, dropsLeft, _, _) = auctionState {
- var minBidAmount = minBidAmount
- if let myMinBidAmmount = self.giftAuctionState?.myState.minBidAmount {
- minBidAmount = myMinBidAmmount
- }
- var minBidString: String
- if minBidAmount > 99999 {
- minBidString = compactNumericCountString(Int(minBidAmount), decimalSeparator: environment.dateTimeFormat.decimalSeparator, showDecimalPart: false)
- } else {
- minBidString = presentationStringsFormattedNumber(Int32(clamping: minBidAmount), environment.dateTimeFormat.groupingSeparator)
- }
- minBidString = "# \(minBidString)"
- if let hashIndex = minBidString.firstIndex(of: "#") {
- var prefix = String(minBidString[.. 99999 {
+ minBidString = compactNumericCountString(Int(minBidAmount), decimalSeparator: environment.dateTimeFormat.decimalSeparator, showDecimalPart: false)
+ } else {
+ minBidString = presentationStringsFormattedNumber(Int32(clamping: minBidAmount), environment.dateTimeFormat.groupingSeparator)
+ }
+ minBidString = "# \(minBidString)"
+ if let hashIndex = minBidString.firstIndex(of: "#") {
+ var prefix = String(minBidString[.. 0 {
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1)))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":")))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":")))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
+ } else {
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon", content: .text(":")))
+ untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
+ }
+
+ if dropsLeft >= 10000 {
+ var compactString = compactNumericCountString(Int(dropsLeft), decimalSeparator: ".", showDecimalPart: false)
+ let suffix = String(compactString.suffix(1))
+ compactString.removeLast()
+ if let value = Int(compactString) {
+ dropsLeftAnimatedItems = [
+ AnimatedTextComponent.Item(id: "drops", content: .number(value, minDigits: 1)),
+ AnimatedTextComponent.Item(id: "suffix", content: .text(suffix))
+ ]
+ }
+ } else {
dropsLeftAnimatedItems = [AnimatedTextComponent.Item(id: "drops", content: .number(Int(dropsLeft), minDigits: 1))]
}
}
- perks.append((
+ auctionStats.append((
minBidAnimatedItems,
environment.strings.Gift_AuctionBid_MinimumBid
))
- perks.append((
- untilNextDropAnimatedItems,
- environment.strings.Gift_AuctionBid_UntilNext
+ auctionStats.append((
+ untilNextRoundAnimatedItems,
+ nextRoundTitle
))
- perks.append((
+ auctionStats.append((
dropsLeftAnimatedItems,
environment.strings.Gift_AuctionBid_Left
))
contentHeight += 54.0
- let perkHeight: CGFloat = 60.0
- let perkSpacing: CGFloat = 10.0
- let perkWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count))
+ let statSpacing: CGFloat = 10.0
+ let statWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - statSpacing * CGFloat(auctionStats.count - 1)) / CGFloat(auctionStats.count))
+ let statHeight: CGFloat = 60.0
- for i in 0 ..< perks.count {
- var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight))
- if i == perks.count - 1 {
- perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX)
+ for i in 0 ..< auctionStats.count {
+ var statFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (statWidth + statSpacing), y: contentHeight), size: CGSize(width: statWidth, height: statHeight))
+ if i == auctionStats.count - 1 {
+ statFrame.size.width = max(0.0, availableSize.width - sideInset - statFrame.minX)
}
- let perkView: ComponentView
- if self.liveStreamPerks.count > i {
- perkView = self.liveStreamPerks[i]
+ let statView: ComponentView
+ if self.auctionStats.count > i {
+ statView = self.auctionStats[i]
} else {
- perkView = ComponentView()
- self.liveStreamPerks.append(perkView)
+ statView = ComponentView()
+ self.auctionStats.append(statView)
}
- let perk = perks[i]
- let _ = perkView.update(
+ let stat = auctionStats[i]
+ let _ = statView.update(
transition: transition,
component: AnyComponent(AuctionStatComponent(
context: component.context,
- gift: i == perks.count - 1 ? component.auctionContext.gift : nil,
- title: perk.0,
- subtitle: perk.1,
+ gift: i == auctionStats.count - 1 ? component.auctionContext.gift : nil,
+ title: stat.0,
+ subtitle: stat.1,
small: false,
theme: environment.theme
)),
environment: {},
- containerSize: perkFrame.size
+ containerSize: statFrame.size
)
- if let perkComponentView = perkView.view {
+ if let perkComponentView = statView.view {
if perkComponentView.superview == nil {
self.scrollContentView.addSubview(perkComponentView)
}
- transition.setFrame(view: perkComponentView, frame: perkFrame)
+ transition.setFrame(view: perkComponentView, frame: statFrame)
}
}
- contentHeight += perkHeight
+ contentHeight += statHeight
contentHeight += 24.0
let acquiredGiftsCount = self.giftAuctionState?.myState.acquiredCount ?? 0
@@ -2655,25 +2707,40 @@ private final class GiftAuctionBidScreenComponent: Component {
var initialContentHeight = contentHeight
let clippingY: CGFloat
+ let titleString: String
+ if isUpcoming {
+ titleString = environment.strings.Gift_AuctionBid_UpcomingTitle
+ } else {
+ titleString = environment.strings.Gift_AuctionBid_Title
+ }
+
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
+ text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
+ var subtitleString = environment.strings.Gift_AuctionBid_Subtitle("\(giftsPerRound)").string
+ if let auctionState = self.giftAuctionState?.auctionState, case let .ongoing(_, _, _, _, _, _, _, _, currentRound, totalRounds, _, _) = auctionState {
+ subtitleString = environment.strings.Gift_AuctionBid_RoundSubtitle(
+ presentationStringsFormattedNumber(currentRound, environment.dateTimeFormat.groupingSeparator),
+ presentationStringsFormattedNumber(totalRounds, environment.dateTimeFormat.groupingSeparator),
+ ).string
+ }
+
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
- text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Subtitle("\(giftsPerRound)").string, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))
+ text: .plain(NSAttributedString(string: isUpcoming ? "" : subtitleString, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
)
- let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 19.0), size: titleSize)
+ let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: isUpcoming ? 27.0 : 19.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView)
@@ -3473,3 +3540,101 @@ private final class GiftViewContextReferenceContentSource: ContextReferenceConte
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
+
+private final class PeerHeaderComponent: CombinedComponent {
+ let color: UIColor
+ let dateTimeFormat: PresentationDateTimeFormat
+ let title: String
+ let giftTitle: String?
+ let giftNumber: Int32?
+
+ public init(
+ color: UIColor,
+ dateTimeFormat: PresentationDateTimeFormat,
+ title: String,
+ giftTitle: String?,
+ giftNumber: Int32?
+ ) {
+ self.color = color
+ self.dateTimeFormat = dateTimeFormat
+ self.title = title
+ self.giftTitle = giftTitle
+ self.giftNumber = giftNumber
+ }
+
+ static func ==(lhs: PeerHeaderComponent, rhs: PeerHeaderComponent) -> Bool {
+ if lhs.color != rhs.color {
+ return false
+ }
+ if lhs.title != rhs.title {
+ return false
+ }
+ if lhs.giftTitle != rhs.giftTitle {
+ return false
+ }
+ if lhs.giftNumber != rhs.giftNumber {
+ return false
+ }
+ return true
+ }
+
+ static var body: Body {
+ let title = Child(MultilineTextComponent.self)
+
+ let background = Child(RoundedRectangle.self)
+ let giftTitle = Child(MultilineTextComponent.self)
+
+ return { context in
+ let component = context.component
+
+ let title = title.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(string: component.title.uppercased(), font: Font.medium(13.0), textColor: component.color))
+ ),
+ availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height),
+ transition: .immediate
+ )
+ context.add(title
+ .position(CGPoint(x: title.size.width / 2.0, y: title.size.height / 2.0))
+ )
+
+ var contentSize = title.size
+ if let titleString = component.giftTitle, let giftNumber = component.giftNumber {
+ let giftTitle = giftTitle.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(string: "\(titleString) #\(formatCollectibleNumber(giftNumber, dateTimeFormat: component.dateTimeFormat))", font: Font.regular(12.0), textColor: component.color))
+ ),
+ availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height),
+ transition: .immediate
+ )
+
+ let spacing: CGFloat = 6.0
+ let padding: CGFloat = 5.0
+ let backgroundSize = CGSize(width: giftTitle.size.width + padding * 2.0, height: giftTitle.size.height + 4.0)
+ let background = background.update(
+ component: RoundedRectangle(
+ color: component.color.withAlphaComponent(0.1),
+ cornerRadius: backgroundSize.height / 2.0
+ ),
+ availableSize: backgroundSize,
+ transition: .immediate
+ )
+ context.add(background
+ .position(CGPoint(x: title.size.width + spacing + padding + giftTitle.size.width / 2.0, y: title.size.height / 2.0))
+ .appear(.default())
+ .disappear(.default())
+ )
+
+ context.add(giftTitle
+ .position(CGPoint(x: title.size.width + spacing + padding + giftTitle.size.width / 2.0, y: title.size.height / 2.0))
+ .appear(.default())
+ .disappear(.default())
+ )
+
+ contentSize.width += spacing + backgroundSize.width
+ }
+
+ return contentSize
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift
index da94c92f4e..e4086accc0 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift
@@ -80,7 +80,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent {
let _ = (self.auctionContext.state
|> deliverOnMainQueue).startStandalone(next: { [weak self] state in
- if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds) = state?.auctionState {
+ if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds, _, _) = state?.auctionState {
self.rounds = totalRounds
self.updated()
}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift
index b461cf3f60..4d11e0eef5 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift
@@ -71,7 +71,19 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
fileprivate var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = []
private var giftAuctionAcquiredGiftsPromise = ValuePromise<[GiftAuctionAcquiredGift]>()
private var giftAuctionAcquiredGiftsDisposable = MetaDisposable()
-
+
+ private(set) var giftUpgradeAttributes: [StarGift.UniqueGift.Attribute]?
+ private var previewTimer: SwiftSignalKit.Timer?
+ private(set) var previewModelIndex: Int = -1
+ private(set) var previewBackdropIndex: Int = -1
+ private(set) var previewSymbolIndex: Int = -1
+
+ private var disposables = DisposableSet()
+ private var fetchedFiles = Set()
+ private(set) var previewModels: [StarGift.UniqueGift.Attribute] = []
+ private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = []
+ private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = []
+
var cachedStarImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
@@ -103,17 +115,97 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
self.loadAcquiredGifts()
}
})
-
+
self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updated()
}, queue: Queue.mainQueue())
self.giftAuctionTimer?.start()
+
+ if case let .generic(gift) = auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount, upgradeVariantsCount > 0 {
+ let _ = (context.engine.payments.getStarGiftUpgradeAttributes(giftId: gift.id)
+ |> deliverOnMainQueue).start(next: { [weak self] attributes in
+ guard let self, let attributes else {
+ return
+ }
+ self.giftUpgradeAttributes = attributes
+
+ let randomModels = Array(attributes.filter({ attribute in
+ if case .model = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled().prefix(5))
+ self.previewModels = randomModels
+
+ let randomBackdrops = Array(attributes.filter({ attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled())
+ self.previewBackdrops = randomBackdrops
+
+ let randomSymbols = Array(attributes.filter({ attribute in
+ if case .pattern = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled().prefix(5))
+ self.previewSymbols = randomSymbols
+
+ for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
+ self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
+ self.fetchedFiles.insert(file.fileId.id)
+ }
+
+ for case let .pattern(_, file, _) in self.previewSymbols where !self.fetchedFiles.contains(file.fileId.id) {
+ self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
+ self.fetchedFiles.insert(file.fileId.id)
+ }
+
+ self.updated()
+ })
+
+ self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.previewTimerTick()
+ }, queue: Queue.mainQueue())
+ self.previewTimer?.start()
+ }
}
deinit {
self.disposable?.dispose()
+ self.disposables.dispose()
self.giftAuctionAcquiredGiftsDisposable.dispose()
self.giftAuctionTimer?.invalidate()
+ self.previewTimer?.invalidate()
+ }
+
+ private func previewTimerTick() {
+ guard !self.previewModels.isEmpty else { return }
+ self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count
+
+ let previousSymbolIndex = self.previewSymbolIndex
+ var randomSymbolIndex = previousSymbolIndex
+ while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty {
+ randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count)
+ }
+ if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex }
+
+ let previousBackdropIndex = self.previewBackdropIndex
+ var randomBackdropIndex = previousBackdropIndex
+ while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
+ randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count)
+ }
+ if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
+
+ self.updated(transition: .easeInOut(duration: 0.25))
}
func loadAcquiredGifts() {
@@ -171,7 +263,14 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
}
self.dismiss(animated: true)
- controller.completion(self.giftAuctionAcquiredGiftsPromise.get())
+ var compactUpgradeAttributes: [StarGift.UniqueGift.Attribute] = []
+ if !self.previewModels.isEmpty {
+ compactUpgradeAttributes.append(contentsOf: self.previewModels)
+ compactUpgradeAttributes.append(contentsOf: self.previewBackdrops)
+ compactUpgradeAttributes.append(contentsOf: self.previewSymbols)
+ }
+
+ controller.completion(self.giftAuctionAcquiredGiftsPromise.get(), !compactUpgradeAttributes.isEmpty ? compactUpgradeAttributes : nil)
}
func openPeer(_ peer: EnginePeer, dismiss: Bool = true) {
@@ -291,6 +390,26 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
controller.present(shareController, in: .window(.root))
}
+ func openGiftResale() {
+ guard let controller = self.getController() as? GiftAuctionViewScreen, let gift = self.giftAuctionState?.gift, case let .generic(gift) = gift else {
+ return
+ }
+ let storeController = self.context.sharedContext.makeGiftStoreController(
+ context: self.context,
+ peerId: self.context.account.peerId,
+ gift: gift
+ )
+ controller.push(storeController)
+ }
+
+ func openGiftFragmentResale() {
+ guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, case let .finished(_, _, _, _, _, fragmentListedUrl) = self.giftAuctionState?.auctionState, let url = fragmentListedUrl else {
+ return
+ }
+ let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
+ self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
+ }
+
func morePressed(view: UIView, gesture: ContextGesture?) {
guard let controller = self.getController() as? GiftAuctionViewScreen else {
return
@@ -355,23 +474,27 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
}
static var body: Body {
+ let header = Child(GiftCompositionComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let moreButton = Child(GlassBarButtonComponent.self)
- let animation = Child(GiftItemComponent.self)
let title = Child(MultilineTextComponent.self)
- let description = Child(BalancedTextComponent.self)
+ let description = Child(StarsButtonContentComponent.self)
+ let learnMore = Child(PlainButtonComponent.self)
let table = Child(TableComponent.self)
let button = Child(ButtonComponent.self)
+ let variantsButton = Child(PlainButtonComponent.self)
let acquiredButton = Child(PlainButtonComponent.self)
-// let telegramSaleButton = Child(PlainButtonComponent.self)
-// let fragmentSaleButton = Child(PlainButtonComponent.self)
+ let telegramSaleButton = Child(PlainButtonComponent.self)
+ let fragmentSaleButton = Child(PlainButtonComponent.self)
let moreButtonPlayOnce = ActionSlot()
+ let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
+
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
@@ -385,83 +508,47 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var titleString: String = ""
- var giftIconSubject: GiftItemComponent.Subject?
var genericGift: StarGift.Gift?
switch component.auctionContext.gift {
case let .generic(gift):
titleString = gift.title ?? ""
- giftIconSubject = .starGift(gift: gift, price: "")
genericGift = gift
default:
break
}
-
- let _ = giftIconSubject
- let _ = genericGift
-
+
var originY: CGFloat = 0.0
-
- if let genericGift {
- let animation = animation.update(
- component: GiftItemComponent(
- context: component.context,
- theme: environment.theme,
- strings: environment.strings,
- subject: .starGift(gift: genericGift, price: ""),
- ribbon: GiftItemComponent.Ribbon(text: strings.Gift_Auction_Auction, color: .orange),
- outline: .orange,
- mode: .header
- ),
- availableSize: CGSize(width: 120.0, height: 120.0),
- transition: context.transition
- )
- context.add(animation
- .position(CGPoint(x: context.availableSize.width / 2.0, y: 92.0))
- )
- }
- originY += 177.0
-
- let title = title.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: titleString,
- font: Font.bold(24.0),
- textColor: theme.list.itemPrimaryTextColor,
- paragraphAlignment: .center
- )),
- horizontalAlignment: .center,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
- transition: .immediate
- )
- context.add(title
- .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0))
- )
+
+ var buttonColor: UIColor = .white.withAlphaComponent(0.1)
+ var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
var descriptionText: String = ""
- var descriptionColor = theme.list.itemSecondaryTextColor
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
+ var startTime = currentTime
var endTime = currentTime
+ var isUpcoming = false
var isEnded = false
var tableItems: [TableComponent.Item] = []
if let auctionState = state.giftAuctionState, case let .generic(gift) = component.auctionContext.gift {
+ startTime = auctionState.startDate
endTime = auctionState.endDate
if case .finished = auctionState.auctionState {
isEnded = true
} else if auctionState.endDate < currentTime {
isEnded = true
}
+ if auctionState.startDate > currentTime {
+ isUpcoming = true
+ }
if isEnded {
descriptionText = strings.Gift_Auction_Ended
- descriptionColor = theme.list.itemDestructiveColor
tableItems.append(.init(
id: "firstSale",
@@ -477,7 +564,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.endDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
- if case let .finished(_, _, averagePrice) = auctionState.auctionState {
+ if case let .finished(_, _, averagePrice, _, _, _) = auctionState.auctionState {
var items: [AnyComponentWithIdentity] = []
let valueString = "\(presentationStringsFormattedNumber(abs(Int32(clamping: averagePrice)), dateTimeFormat.groupingSeparator))⭐️"
@@ -531,49 +618,197 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
)
))
} else {
- var auctionGiftsPerRound: Int32 = 50
- if let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound {
- auctionGiftsPerRound = auctionGiftsPerRoundValue
- }
- descriptionText = strings.Gift_Auction_Description("\(auctionGiftsPerRound)", gift.title ?? "").string
+ descriptionText = isUpcoming ? strings.Gift_Auction_UpcomingAuction : strings.Gift_Auction_GiftAuction
tableItems.append(.init(
id: "start",
- title: strings.Gift_Auction_Started,
+ title: isUpcoming ? strings.Gift_Auction_Start : strings.Gift_Auction_Started,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.startDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
tableItems.append(.init(
id: "ends",
- title: strings.Gift_Auction_Ends,
+ title: isUpcoming ? strings.Gift_Auction_End : strings.Gift_Auction_Ends,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.endDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
- if case let .ongoing(_, _, _, _, _, _, _, giftsLeft, currentRound, totalRounds) = auctionState.auctionState {
- tableItems.append(.init(
- id: "round",
- title: strings.Gift_Auction_CurrentRound,
- component: AnyComponent(
- MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_Round("\(currentRound)", "\(totalRounds)").string, font: tableFont, textColor: tableTextColor)))
- )
- ))
- tableItems.append(.init(
- id: "availability",
- title: strings.Gift_Auction_Availability,
- component: AnyComponent(
- MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf(presentationStringsFormattedNumber(giftsLeft, dateTimeFormat.groupingSeparator), presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor)))
- )
- ))
+
+ if case let .ongoing(_, _, _, _, _, _, _, giftsLeft, currentRound, totalRounds, rounds, _) = auctionState.auctionState {
+ if isUpcoming {
+ tableItems.append(.init(
+ id: "quantity",
+ title: strings.Gift_Auction_Quantity,
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator), font: tableFont, textColor: tableTextColor)))
+ )
+ ))
+ tableItems.append(.init(
+ id: "rounds",
+ title: strings.Gift_Auction_TotalRounds,
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(totalRounds, dateTimeFormat.groupingSeparator), font: tableFont, textColor: tableTextColor)))
+ )
+ ))
+
+ for i in 0 ..< rounds.count {
+ let round = rounds[i]
+ let start = round.num
+ var end = totalRounds
+ if i < rounds.count - 1 {
+ let nextRound = rounds[i + 1]
+ end = nextRound.num - 1
+ }
+
+ let title: String = start == end ? strings.Gift_Auction_TimeRound("\(start)").string : strings.Gift_Auction_TimeRounds("\(start)-\(end)").string
+ var value: String
+ if round.duration % 3600 == 0 {
+ let hours = round.duration / 3600
+ value = start == end ? strings.Gift_Auction_Hours(hours) : strings.Gift_Auction_HoursEach(hours)
+ } else {
+ let minutes = round.duration / 60
+ value = start == end ? strings.Gift_Auction_Minutes(minutes) : strings.Gift_Auction_MinutesEach(minutes)
+ }
+
+ if case let .extendable(_, _, top, window) = round {
+ var windowString: String
+ if window % 60 == 0 {
+ windowString = strings.Gift_Auction_Minutes(window)
+ } else {
+ windowString = strings.Gift_Auction_Seconds(window)
+ }
+ value += " \(strings.Gift_Auction_Extension(windowString, "\(top)").string)"
+ }
+
+ tableItems.append(.init(
+ id: "round_\(i)",
+ title: title,
+ component: AnyComponent(
+ MultilineTextComponent(
+ text: .plain(NSAttributedString(string: value, font: tableFont, textColor: tableTextColor)),
+ maximumNumberOfLines: 3
+ )
+ )
+ ))
+ }
+
+ } else {
+ tableItems.append(.init(
+ id: "round",
+ title: strings.Gift_Auction_CurrentRound,
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_Round("\(currentRound)", "\(totalRounds)").string, font: tableFont, textColor: tableTextColor)))
+ )
+ ))
+ tableItems.append(.init(
+ id: "availability",
+ title: strings.Gift_Auction_Availability,
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf(presentationStringsFormattedNumber(giftsLeft, dateTimeFormat.groupingSeparator), presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor)))
+ )
+ ))
+ }
}
}
}
-
- let textFont = Font.regular(15.0)
- let boldTextFont = Font.semibold(15.0)
- let textColor = descriptionColor
- let linkColor = theme.list.itemAccentColor
+
+ if let genericGift {
+ var attributes: [StarGift.UniqueGift.Attribute] = []
+ if state.previewModelIndex == -1 {
+ attributes.append(.model(name: "", file: genericGift.file, rarity: 0))
+ if let background = genericGift.background {
+ attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0))
+ }
+ } else if !state.previewModels.isEmpty {
+ attributes.append(state.previewModels[state.previewModelIndex])
+ if !state.previewBackdrops.isEmpty {
+ attributes.append(state.previewBackdrops[state.previewBackdropIndex])
+ }
+ if !state.previewSymbols.isEmpty {
+ attributes.append(state.previewSymbols[state.previewSymbolIndex])
+ }
+ }
+
+ if let backdropAttribute = attributes.first(where: { attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
+ let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
+ let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89)
+ buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25)
+
+ secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5)
+ }
+
+ let headerHeight: CGFloat = 264.0
+
+ let header = header.update(
+ component: GiftCompositionComponent(
+ context: component.context,
+ theme: theme,
+ subject: .preview(attributes),
+ animationOffset: CGPoint(x: 0.0, y: -4.0),
+ animationScale: nil,
+ displayAnimationStars: false,
+ revealedAttributes: Set(),
+ externalState: giftCompositionExternalState,
+ requestUpdate: { [weak state] transition in
+ state?.updated(transition: transition)
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
+ transition: context.transition
+ )
+ context.add(header
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height * 0.5))
+ )
+ originY += header.size.height
+ originY += 16.0
+ }
+
+ let title = title.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: titleString,
+ font: Font.bold(20.0),
+ textColor: .white,
+ paragraphAlignment: .center
+ )),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 1
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
+ transition: .immediate
+ )
+ context.add(title
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 209.0))
+ )
+
+ let description = description.update(
+ component: StarsButtonContentComponent(
+ context: component.context,
+ text: descriptionText,
+ color: buttonColor,
+ tinted: true,
+ starsColor: isEnded ? .clear : UIColor(white: 1.0, alpha: 0.5),
+ font: Font.medium(13.0),
+ height: 24.0
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
+ transition: context.transition
+ )
+ context.add(description
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 167.0 + description.size.height / 2.0))
+ )
+
+ let textFont = Font.regular(13.0)
+ let boldTextFont = Font.semibold(13.0)
+ let textColor = UIColor.white
+ let linkColor = UIColor.white
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
@@ -584,48 +819,32 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
- descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
- let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
- if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
- attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
+ let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_Auction_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
+ if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
+ learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string))
}
-
- let description = description.update(
- component: BalancedTextComponent(
- text: .plain(attributedString),
- maximumNumberOfLines: 0,
- lineSpacing: 0.2,
- highlightColor: linkColor.withAlphaComponent(0.1),
- highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
- highlightAction: { attributes in
- if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
- return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
- } else {
- return nil
- }
+ let learnMore = learnMore.update(
+ component: PlainButtonComponent(
+ content: AnyComponent(BalancedTextComponent(
+ text: .plain(learnMoreAttributedString),
+ maximumNumberOfLines: 1,
+ tintColor: secondaryTextColor
+ )),
+ action: {
+ let controller = component.context.sharedContext.makeGiftDemoScreen(
+ context: component.context
+ )
+ environment.controller()?.push(controller)
},
- tapAction: { attributes, _ in
- if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
- let controller = component.context.sharedContext.makeGiftAuctionInfoScreen(
- context: component.context,
- auctionContext: component.auctionContext,
- completion: nil
- )
- environment.controller()?.push(controller)
- }
- }
+ animateScale: false
),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
- transition: .immediate
+ availableSize: context.availableSize,
+ transition: context.transition
)
- context.add(description
- .position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0 + description.size.height / 2.0))
- .appear(.default(alpha: true))
- .disappear(.default(alpha: true))
+ context.add(learnMore
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 238.0))
)
- originY += description.size.height
- originY += 42.0
let table = table.update(
component: TableComponent(
@@ -637,8 +856,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
)
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
- .appear(.default(alpha: true))
- .disappear(.default(alpha: true))
)
originY += table.size.height + 26.0
@@ -692,6 +909,188 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
hasAdditionalButtons = true
}
+ if case let .finished(_, _, _, listedCount, fragmentListedCount, _) = state.giftAuctionState?.auctionState, case let .generic(gift) = component.auctionContext.gift {
+ if let listedCount = listedCount, listedCount > 0 {
+ originY += 5.0
+
+ let telegramSaleButton = telegramSaleButton.update(
+ component: PlainButtonComponent(content: AnyComponent(
+ HStack([
+ AnyComponentWithIdentity(id: "count", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
+ Rectangle(color: .clear, width: 8.0, height: 1.0)
+ )),
+ AnyComponentWithIdentity(id: "icon", component: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ peer: nil,
+ subject: .starGift(gift: gift, price: ""),
+ mode: .buttonIcon
+ )
+ )),
+ AnyComponentWithIdentity(id: "text", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
+ BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
+ ))
+ ], spacing: 0.0)
+ ), action: { [weak state] in
+ guard let state else {
+ return
+ }
+ state.openGiftResale()
+ }, animateScale: false),
+ availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
+ transition: context.transition
+ )
+ context.add(telegramSaleButton
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0)))
+ originY += telegramSaleButton.size.height
+ originY += 12.0
+
+ hasAdditionalButtons = true
+ }
+
+ if let listedCount = fragmentListedCount, listedCount > 0 {
+ let fragmentSaleButton = fragmentSaleButton.update(
+ component: PlainButtonComponent(content: AnyComponent(
+ HStack([
+ AnyComponentWithIdentity(id: "count", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
+ Rectangle(color: .clear, width: 8.0, height: 1.0)
+ )),
+ AnyComponentWithIdentity(id: "icon", component: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ peer: nil,
+ subject: .starGift(gift: gift, price: ""),
+ mode: .buttonIcon
+ )
+ )),
+ AnyComponentWithIdentity(id: "text", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
+ BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor)
+ ))
+ ], spacing: 0.0)
+ ), action: { [weak state] in
+ guard let state else {
+ return
+ }
+ state.openGiftFragmentResale()
+ }, animateScale: false),
+ availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
+ transition: context.transition
+ )
+ context.add(fragmentSaleButton
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0)))
+ originY += fragmentSaleButton.size.height
+ originY += 12.0
+
+ hasAdditionalButtons = true
+ }
+ }
+
+ if case let .generic(gift) = component.auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount {
+ originY += 5.0
+
+ if !hasAdditionalButtons {
+ originY -= 13.0
+ }
+
+ var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
+ var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
+ var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "")
+ if !state.previewModels.isEmpty {
+ if state.previewModels.count > 0 {
+ variant1 = .preview(attributes: [state.previewModels[0]], rarity: 0)
+ }
+ if state.previewModels.count > 1 {
+ variant2 = .preview(attributes: [state.previewModels[1]], rarity: 0)
+ }
+ if state.previewModels.count > 2 {
+ variant3 = .preview(attributes: [state.previewModels[2]], rarity: 0)
+ }
+ }
+
+ let variantsButton = variantsButton.update(
+ component: PlainButtonComponent(content: AnyComponent(
+ HStack([
+ AnyComponentWithIdentity(id: "view", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_ViewVariants, font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "spacing", component: AnyComponent(
+ Rectangle(color: .clear, width: 7.0, height: 1.0)
+ )),
+ AnyComponentWithIdentity(id: "icon1", component: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ peer: nil,
+ subject: variant1,
+ isPlaceholder: state.previewModels.isEmpty,
+ mode: .tableIcon
+ )
+ )),
+ AnyComponentWithIdentity(id: "icon2", component: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ peer: nil,
+ subject: variant2,
+ isPlaceholder: state.previewModels.isEmpty,
+ mode: .tableIcon
+ )
+ )),
+ AnyComponentWithIdentity(id: "icon3", component: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ peer: nil,
+ subject: variant3,
+ isPlaceholder: state.previewModels.isEmpty,
+ mode: .tableIcon
+ )
+ )),
+ AnyComponentWithIdentity(id: "text", component: AnyComponent(
+ MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Auction_Variants(upgradeVariantsCount))", font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor)))
+ )),
+ AnyComponentWithIdentity(id: "arrow", component: AnyComponent(
+ BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: theme.actionSheet.controlAccentColor)
+ ))
+ ], spacing: 0.0)
+ ), action: { [weak state] in
+ guard let state, let attributes = state.giftUpgradeAttributes else {
+ return
+ }
+ let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsPreviewScreen(context: component.context, gift: .generic(gift), attributes: attributes)
+ environment.controller()?.push(variantsController)
+ }, animateScale: false),
+ availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
+ transition: context.transition
+ )
+ context.add(variantsButton
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + variantsButton.size.height / 2.0)))
+ originY += variantsButton.size.height
+ originY += 12.0
+ originY -= 15.0
+
+ hasAdditionalButtons = true
+ }
+
if hasAdditionalButtons {
originY += 21.0
}
@@ -707,21 +1106,32 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
let buttonChild: _UpdatedChildComponent
if !isEnded {
- let buttonAttributedString = NSMutableAttributedString(string: strings.Gift_Auction_Join, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
+ let buttonAttributedString = NSMutableAttributedString(string: isUpcoming ? strings.Gift_Auction_EarlyBid : strings.Gift_Auction_Join, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
- let endTimeout = max(0, endTime - currentTime)
+ let endTimeout: Int32
+ if currentTime < startTime {
+ endTimeout = max(0, startTime - currentTime)
+ } else {
+ endTimeout = max(0, endTime - currentTime)
+ }
let hours = Int(endTimeout / 3600)
let minutes = Int((endTimeout % 3600) / 60)
let seconds = Int(endTimeout % 60)
- let rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
+ let rawString: String
+ if isUpcoming {
+ rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes
+ } else {
+ rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
+ }
+
var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
var startIndex = rawString.startIndex
while true {
if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
if range.lowerBound != startIndex {
- buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
}
startIndex = range.upperBound
@@ -742,7 +1152,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
}
}
if startIndex != rawString.endIndex {
- buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
}
let items: [AnyComponentWithIdentity] = [
@@ -759,7 +1169,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
component: ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
- id: AnyHashable("buy"),
+ id: AnyHashable("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,
@@ -803,13 +1213,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
- backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
- isDark: theme.overallDarkAppearance,
- state: .generic,
+ backgroundColor: buttonColor,
+ isDark: false,
+ state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
- tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
+ tintColor: .white
)
)),
action: { [weak state] _ in
@@ -820,7 +1230,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
- transition: .immediate
+ transition: context.transition
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
@@ -829,15 +1239,15 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
let moreButton = moreButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
- backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
- isDark: theme.overallDarkAppearance,
- state: .generic,
+ backgroundColor: buttonColor,
+ isDark: false,
+ state: .tintedGlass,
component: AnyComponentWithIdentity(id: "more", component: AnyComponent(
LottieComponent(
content: LottieComponent.AppBundleContent(
name: "anim_morewide"
),
- color: theme.rootController.navigationBar.glassBarButtonForegroundColor,
+ color: .white,
size: CGSize(width: 34.0, height: 34.0),
playOnce: moreButtonPlayOnce
)
@@ -851,7 +1261,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
- transition: .immediate
+ transition: context.transition
)
context.add(moreButton
.position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 16.0 + moreButton.size.height / 2.0))
@@ -977,12 +1387,12 @@ final class GiftAuctionViewSheetComponent: CombinedComponent {
}
public final class GiftAuctionViewScreen: ViewControllerComponentContainer {
- fileprivate let completion: (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void
+ fileprivate let completion: (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void
public init(
context: AccountContext,
auctionContext: GiftAuctionContext,
- completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void
+ completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void
) {
self.completion = completion
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift
new file mode 100644
index 0000000000..ffb88c3b4b
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift
@@ -0,0 +1,788 @@
+import Foundation
+import UIKit
+import Display
+import AsyncDisplayKit
+import Postbox
+import TelegramCore
+import SwiftSignalKit
+import AccountContext
+import TelegramPresentationData
+import PresentationDataUtils
+import ComponentFlow
+import ViewControllerComponent
+import SheetComponent
+import MultilineTextComponent
+import MultilineTextWithEntitiesComponent
+import BundleIconComponent
+import ButtonComponent
+import Markdown
+import BalancedTextComponent
+import AvatarNode
+import TextFormat
+import TelegramStringFormatting
+import PlainButtonComponent
+import GiftItemComponent
+import GiftAnimationComponent
+import GlassBarButtonComponent
+import GiftRemainingCountComponent
+import AnimatedTextComponent
+
+private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let auctionContext: GiftAuctionContext
+ let attributes: [StarGift.UniqueGift.Attribute]
+ let animateOut: ActionSlot>
+ let getController: () -> ViewController?
+
+ init(
+ context: AccountContext,
+ auctionContext: GiftAuctionContext,
+ attributes: [StarGift.UniqueGift.Attribute],
+ animateOut: ActionSlot>,
+ getController: @escaping () -> ViewController?
+ ) {
+ self.context = context
+ self.auctionContext = auctionContext
+ self.attributes = attributes
+ self.animateOut = animateOut
+ self.getController = getController
+ }
+
+ static func ==(lhs: GiftAuctionWearPreviewSheetContent, rhs: GiftAuctionWearPreviewSheetContent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ return true
+ }
+
+ final class State: ComponentState {
+ private let context: AccountContext
+
+ private var peerDisposable: Disposable?
+ var peerMap: [EnginePeer.Id: EnginePeer] = [:]
+
+ var cachedSmallChevronImage: (UIImage, PresentationTheme)?
+
+ private var disposable: Disposable?
+ private(set) var giftAuctionState: GiftAuctionContext.State?
+ private var giftAuctionTimer: SwiftSignalKit.Timer?
+
+ private var previewTimer: SwiftSignalKit.Timer?
+ private(set) var previewModelIndex: Int = 0
+ private(set) var previewBackdropIndex: Int = 0
+ private(set) var previewSymbolIndex: Int = 0
+
+ private(set) var previewModels: [StarGift.UniqueGift.Attribute] = []
+ private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = []
+ private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = []
+
+ init(
+ context: AccountContext,
+ auctionContext: GiftAuctionContext,
+ attributes: [StarGift.UniqueGift.Attribute]
+ ) {
+ self.context = context
+
+ super.init()
+
+ for attribute in attributes {
+ switch attribute {
+ case .model:
+ self.previewModels.append(attribute)
+ case .backdrop:
+ self.previewBackdrops.append(attribute)
+ case .pattern:
+ self.previewSymbols.append(attribute)
+ default:
+ break
+ }
+ }
+
+ let peerIds: [EnginePeer.Id] = [context.account.peerId]
+ self.peerDisposable = (
+ context.engine.data.get(EngineDataMap(
+ peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
+ return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
+ }
+ ))
+ |> deliverOnMainQueue
+ ).startStrict(next: { [weak self] peers in
+ if let strongSelf = self {
+ var peersMap: [EnginePeer.Id: EnginePeer] = [:]
+ for (peerId, maybePeer) in peers {
+ if let peer = maybePeer {
+ peersMap[peerId] = peer
+ }
+ }
+ strongSelf.peerMap = peersMap
+ strongSelf.updated(transition: .immediate)
+ }
+ })
+
+ self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.previewTimerTick()
+ }, queue: Queue.mainQueue())
+ self.previewTimer?.start()
+
+ self.disposable = (auctionContext.state
+ |> deliverOnMainQueue).start(next: { [weak self] auctionState in
+ guard let self else {
+ return
+ }
+ self.giftAuctionState = auctionState
+ self.updated()
+ })
+
+ self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
+ self?.updated()
+ }, queue: Queue.mainQueue())
+ self.giftAuctionTimer?.start()
+ }
+
+ deinit {
+ self.disposable?.dispose()
+ self.giftAuctionTimer?.invalidate()
+ self.peerDisposable?.dispose()
+ self.previewTimer?.invalidate()
+ }
+
+ private func previewTimerTick() {
+ guard !self.previewModels.isEmpty else { return }
+ self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count
+
+ let previousSymbolIndex = self.previewSymbolIndex
+ var randomSymbolIndex = previousSymbolIndex
+ while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty {
+ randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count)
+ }
+ if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex }
+
+ let previousBackdropIndex = self.previewBackdropIndex
+ var randomBackdropIndex = previousBackdropIndex
+ while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
+ randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count)
+ }
+ if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
+
+ self.updated(transition: .easeInOut(duration: 0.25))
+ }
+ }
+
+
+ func makeState() -> State {
+ return State(context: self.context, auctionContext: self.auctionContext, attributes: self.attributes)
+ }
+
+ static var body: Body {
+ let closeButton = Child(GlassBarButtonComponent.self)
+ let animation = Child(GiftCompositionComponent.self)
+ let avatar = Child(AvatarComponent.self)
+ let peerName = Child(MultilineTextComponent.self)
+ let learnMore = Child(PlainButtonComponent.self)
+
+ let initialGift = Child(GiftItemComponent.self)
+ let upgradedGift = Child(GiftItemComponent.self)
+ let arrow = Child(BundleIconComponent.self)
+ let upgradeLabel = Child(MultilineTextComponent.self)
+ let remainingCount = Child(GiftRemainingCountComponent.self)
+ let auctionFooter = Child(MultilineTextComponent.self)
+
+ let button = Child(ButtonComponent.self)
+
+ let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
+
+ return { context in
+ let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
+ let component = context.component
+ let theme = environment.theme
+ let strings = environment.strings
+ let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder
+ let controller = environment.controller
+ let state = context.state
+
+ let sideInset: CGFloat = 16.0 + environment.safeInsets.left
+
+ var contentHeight: CGFloat = 0.0
+
+ let headerHeight: CGFloat = 226.0
+
+ var peerNameString = ""
+ if let peer = state.peerMap[component.context.account.peerId] {
+ peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
+ }
+
+ let peerName = peerName.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: peerNameString,
+ font: Font.bold(20.0),
+ textColor: .white,
+ paragraphAlignment: .center
+ )),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 1
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
+ transition: .immediate
+ )
+
+ let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0)
+ let animationScale: CGFloat = 0.19
+
+ var attributes: [StarGift.UniqueGift.Attribute] = []
+ if !state.previewModels.isEmpty {
+ attributes.append(state.previewModels[state.previewModelIndex])
+ if !state.previewBackdrops.isEmpty {
+ attributes.append(state.previewBackdrops[state.previewBackdropIndex])
+ }
+ if !state.previewSymbols.isEmpty {
+ attributes.append(state.previewSymbols[state.previewSymbolIndex])
+ }
+ }
+ let animation = animation.update(
+ component: GiftCompositionComponent(
+ context: component.context,
+ theme: environment.theme,
+ subject: .preview(attributes),
+ animationOffset: animationOffset,
+ animationScale: animationScale,
+ displayAnimationStars: true,
+ animateScaleOnTransition: false,
+ externalState: giftCompositionExternalState,
+ requestUpdate: { [weak state] transition in
+ state?.updated(transition: transition)
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width, height: headerHeight),
+ transition: context.transition
+ )
+ context.add(animation
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0))
+ )
+ contentHeight += headerHeight
+
+ if let peer = state.peerMap[component.context.account.peerId] {
+ let avatar = avatar.update(
+ component: AvatarComponent(
+ context: component.context,
+ theme: theme,
+ peer: peer
+ ),
+ environment: {},
+ availableSize: CGSize(width: 100.0, height: 100.0),
+ transition: context.transition
+ )
+ context.add(avatar
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0))
+ )
+ }
+ context.add(peerName
+ .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0))
+ )
+
+ var buttonColor: UIColor = .white.withAlphaComponent(0.1)
+ var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
+ if let backdropAttribute = attributes.first(where: { attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
+ let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
+ let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89)
+ buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8)
+
+ secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5)
+ }
+
+ let closeButton = closeButton.update(
+ component: GlassBarButtonComponent(
+ size: CGSize(width: 40.0, height: 40.0),
+ backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2),
+ isDark: theme.overallDarkAppearance,
+ state: .tintedGlass,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: .white
+ )
+ )),
+ action: { _ in
+ (controller() as? GiftAuctionWearPreviewScreen)?.dismissAnimated()
+ }
+ ),
+ availableSize: CGSize(width: 40.0, height: 40.0),
+ transition: .immediate
+ )
+ context.add(closeButton
+ .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
+ )
+
+ let textFont = Font.regular(13.0)
+ let boldTextFont = Font.semibold(13.0)
+ let textColor = UIColor.white
+ let linkColor = UIColor.white
+ let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
+ return (TelegramTextAttributes.URL, contents)
+ })
+ if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
+ state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
+ }
+
+ let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_WearPreview_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
+ if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
+ learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string))
+ }
+ let learnMore = learnMore.update(
+ component: PlainButtonComponent(
+ content: AnyComponent(BalancedTextComponent(
+ text: .plain(learnMoreAttributedString),
+ maximumNumberOfLines: 1,
+ tintColor: secondaryTextColor
+ )),
+ action: {
+ let controller = component.context.sharedContext.makeGiftWearPreviewScreen(context: component.context, gift: component.auctionContext.gift, attributes: component.attributes)
+ environment.controller()?.push(controller)
+ },
+ animateScale: false
+ ),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+ context.add(learnMore
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0))
+ )
+
+ if case let .generic(gift) = component.auctionContext.gift {
+ let initialGift = initialGift.update(
+ component: GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ subject: .starGift(gift: gift, price: ""),
+ ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Limited, color: .blue),
+ mode: .thumbnail
+ ),
+ availableSize: CGSize(width: 120.0, height: 120.0),
+ transition: context.transition
+ )
+ context.add(initialGift
+ .position(CGPoint(x: sideInset + initialGift.size.width * 0.5, y: contentHeight + 76.0))
+ )
+ }
+
+ var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
+ for attribute in attributes {
+ if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
+ ribbonColor = .custom(outerColor, innerColor)
+ break
+ }
+ }
+ let upgradedGift = upgradedGift.update(
+ component: GiftItemComponent(
+ context: component.context,
+ theme: theme,
+ strings: strings,
+ subject: .preview(attributes: attributes, rarity: 0),
+ ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Upgraded, color: ribbonColor),
+ animateChanges: true,
+ mode: .thumbnail
+ ),
+ availableSize: CGSize(width: 120.0, height: 120.0),
+ transition: context.transition
+ )
+ context.add(upgradedGift
+ .position(CGPoint(x: context.availableSize.width - sideInset - upgradedGift.size.width * 0.5, y: contentHeight + 76.0))
+ )
+
+
+ let arrow = arrow.update(
+ component: BundleIconComponent(name: "Premium/Auction/Upgrade", tintColor: theme.list.itemSecondaryTextColor),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+ context.add(arrow
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 56.0))
+ )
+
+ let upgradeLabel = upgradeLabel.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(string: strings.Gift_WearPreview_FreeUpgrade, font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 2,
+ lineSpacing: 0.1
+ ),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+ context.add(upgradeLabel
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 94.0))
+ )
+
+ contentHeight += 137.0
+
+ if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability {
+ var remains: Int32 = availability.remains
+ if let auctionState = state.giftAuctionState {
+ switch auctionState.auctionState {
+ case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _):
+ remains = giftsLeft
+ case .finished:
+ remains = 0
+ }
+ }
+ let total = availability.total
+ let position = CGFloat(remains) / CGFloat(total)
+ let sold = total - remains
+ let remainingCount = remainingCount.update(
+ component: GiftRemainingCountComponent(
+ inactiveColor: theme.list.itemBlocksBackgroundColor,
+ activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)],
+ inactiveTitle: strings.Gift_Send_Remains(remains),
+ inactiveValue: "",
+ inactiveTitleColor: theme.list.itemSecondaryTextColor,
+ activeTitle: "",
+ activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "",
+ activeTitleColor: .white,
+ badgeText: "",
+ badgePosition: position,
+ badgeGraphPosition: position,
+ invertProgress: true,
+ leftString: "",
+ groupingSeparator: environment.dateTimeFormat.groupingSeparator
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
+ transition: context.transition
+ )
+ context.add(remainingCount
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight))
+ )
+
+ if let giftsPerRound = gift.auctionGiftsPerRound {
+ let footerAttributes = MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.freeTextColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.list.freeTextColor),
+ link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor),
+ linkAttribute: { contents in
+ return (TelegramTextAttributes.URL, contents)
+ }
+ )
+ let parsedString = parseMarkdownIntoAttributedString(strings.Gift_Setup_AuctionInfo(environment.strings.Gift_Setup_AuctionInfo_Gifts(giftsPerRound), strings.Gift_Setup_AuctionInfo_Bidders(giftsPerRound)).string, attributes: footerAttributes)
+ let auctionFooterText = NSMutableAttributedString(attributedString: parsedString)
+
+ if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
+ state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor)!, environment.theme)
+ }
+ if let range = auctionFooterText.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
+ auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string))
+ }
+
+ let auctionFooter = auctionFooter.update(
+ component: MultilineTextComponent(
+ text: .plain(auctionFooterText),
+ maximumNumberOfLines: 0,
+ highlightColor: theme.list.itemAccentColor.withAlphaComponent(0.1),
+ highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
+ highlightAction: { attributes in
+ if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
+ return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
+ } else {
+ return nil
+ }
+ },
+ tapAction: { _, _ in
+ guard let controller = controller() else {
+ return
+ }
+ let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, auctionContext: component.auctionContext, completion: nil)
+ controller.push(infoController)
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0),
+ transition: context.transition
+ )
+ context.add(auctionFooter
+ .position(CGPoint(x: sideInset + 16.0 + auctionFooter.size.width * 0.5, y: contentHeight + 52.0 + auctionFooter.size.height * 0.5))
+ )
+ contentHeight += auctionFooter.size.height
+ }
+ }
+ contentHeight += 80.0
+
+ let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
+ var startTime = currentTime
+ var endTime = currentTime
+ var isUpcoming = false
+
+ if let auctionState = state.giftAuctionState {
+ startTime = auctionState.startDate
+ endTime = auctionState.endDate
+ }
+
+ var buttonTitle = strings.Gift_Auction_Join
+ let endTimeout: Int32
+ if currentTime < startTime {
+ isUpcoming = true
+ endTimeout = max(0, startTime - currentTime)
+ } else {
+ endTimeout = max(0, endTime - currentTime)
+ }
+
+ let hours = Int(endTimeout / 3600)
+ let minutes = Int((endTimeout % 3600) / 60)
+ let seconds = Int(endTimeout % 60)
+
+ let rawString: String
+ if isUpcoming {
+ buttonTitle = strings.Gift_Auction_EarlyBid
+ rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes
+ } else {
+ rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes
+ }
+
+ var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = []
+ var startIndex = rawString.startIndex
+ while true {
+ if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) {
+ if range.lowerBound != startIndex {
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound]))))
+ }
+
+ startIndex = range.upperBound
+ if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) {
+ let controlString = rawString[range.upperBound ..< endRange.lowerBound]
+ if controlString == "h" {
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2)))
+ } else if controlString == "m" {
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2)))
+ } else if controlString == "s" {
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2)))
+ }
+
+ startIndex = endRange.upperBound
+ }
+ } else {
+ break
+ }
+ }
+ if startIndex != rawString.endIndex {
+ buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex]))))
+ }
+
+ let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
+ let items: [AnyComponentWithIdentity] = [
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
+ AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent(
+ font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers),
+ color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7),
+ items: buttonAnimatedTitleItems,
+ noDelay: true
+ )))
+ ]
+
+ let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
+ let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
+ let buttonBackground = ButtonComponent.Background(
+ style: .glass,
+ color: theme.list.itemCheckColors.fillColor,
+ foreground: theme.list.itemCheckColors.foregroundColor,
+ pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
+ )
+ let button = button.update(
+ component: ButtonComponent(
+ background: buttonBackground,
+ content: AnyComponentWithIdentity(
+ id: AnyHashable("bid"),
+ component: AnyComponent(VStack(items, spacing: 1.0))
+ ),
+ isEnabled: true,
+ displaysProgress: false,
+ action: {
+ if let controller = controller() as? GiftAuctionWearPreviewScreen {
+ controller.completion()
+ controller.dismissAnimated()
+ }
+ }),
+ availableSize: buttonSize,
+ transition: .spring(duration: 0.2)
+ )
+
+ let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size)
+ context.add(button
+ .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
+ )
+ contentHeight += button.size.height
+ contentHeight += 7.0
+
+ let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
+ return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset)
+ }
+ }
+}
+
+final class GiftAuctionWearPreviewSheetComponent: CombinedComponent {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let auctionContext: GiftAuctionContext
+ let attributes: [StarGift.UniqueGift.Attribute]
+
+ init(
+ context: AccountContext,
+ auctionContext: GiftAuctionContext,
+ attributes: [StarGift.UniqueGift.Attribute]
+ ) {
+ self.context = context
+ self.auctionContext = auctionContext
+ self.attributes = attributes
+ }
+
+ static func ==(lhs: GiftAuctionWearPreviewSheetComponent, rhs: GiftAuctionWearPreviewSheetComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ return true
+ }
+
+ static var body: Body {
+ let sheet = Child(SheetComponent.self)
+ let animateOut = StoredActionSlot(Action.self)
+
+ let sheetExternalState = SheetComponent.ExternalState()
+
+ return { context in
+ let environment = context.environment[EnvironmentType.self]
+ let controller = environment.controller
+ let theme = environment.theme.withModalBlocksBackground()
+
+ let sheet = sheet.update(
+ component: SheetComponent(
+ content: AnyComponent(GiftAuctionWearPreviewSheetContent(
+ context: context.component.context,
+ auctionContext: context.component.auctionContext,
+ attributes: context.component.attributes,
+ animateOut: animateOut,
+ getController: controller
+ )),
+ style: .glass,
+ backgroundColor: .color(theme.list.blocksBackgroundColor),
+ followContentSizeChanges: true,
+ clipsContent: true,
+ hasDimView: false,
+ autoAnimateOut: false,
+ externalState: sheetExternalState,
+ animateOut: animateOut,
+ onPan: {
+ },
+ willDismiss: {
+ if let controller = controller() as? GiftAuctionWearPreviewScreen {
+ controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
+ }
+ }
+ ),
+ environment: {
+ environment
+ SheetComponentEnvironment(
+ isDisplaying: environment.value.isVisible,
+ isCentered: environment.metrics.widthClass == .regular,
+ hasInputHeight: !environment.inputHeight.isZero,
+ regularMetricsSize: CGSize(width: 430.0, height: 900.0),
+ dismiss: { animated in
+ if animated {
+ if let controller = controller() as? GiftAuctionWearPreviewScreen {
+ controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition()))
+ animateOut.invoke(Action { _ in
+ controller.dismiss(completion: nil)
+ })
+ }
+ } else {
+ if let controller = controller() as? GiftAuctionWearPreviewScreen {
+ controller.dismiss(completion: nil)
+ }
+ }
+ }
+ )
+ },
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+
+ context.add(sheet
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
+ )
+
+ if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
+ var sideInset: CGFloat = 0.0
+ var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight)
+ if case .regular = environment.metrics.widthClass {
+ sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0
+ bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight
+ }
+
+ let layout = ContainerViewLayout(
+ size: context.availableSize,
+ metrics: environment.metrics,
+ deviceMetrics: environment.deviceMetrics,
+ intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
+ safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)),
+ additionalInsets: .zero,
+ statusBarHeight: environment.statusBarHeight,
+ inputHeight: nil,
+ inputHeightIsInteractivellyChanging: false,
+ inVoiceOver: false
+ )
+ controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
+ }
+
+ return context.availableSize
+ }
+ }
+}
+
+public class GiftAuctionWearPreviewScreen: ViewControllerComponentContainer {
+ private let context: AccountContext
+ fileprivate let completion: () -> Void
+
+ public init(
+ context: AccountContext,
+ auctionContext: GiftAuctionContext,
+ attributes: [StarGift.UniqueGift.Attribute],
+ completion: @escaping () -> Void
+ ) {
+ self.context = context
+ self.completion = completion
+
+ super.init(
+ context: context,
+ component: GiftAuctionWearPreviewSheetComponent(context: context, auctionContext: auctionContext, attributes: attributes),
+ navigationBarAppearance: .none,
+ statusBarStyle: .ignore,
+ theme: .default
+ )
+
+ self.navigationPresentation = .flatModal
+ self.automaticallyControlPresentationContextLayout = false
+ }
+
+ required public init(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.view.disablesInteractiveModalDismiss = true
+ }
+
+ public func dismissAnimated() {
+ if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View {
+ view.dismissAnimated()
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift
new file mode 100644
index 0000000000..7f9a368d2a
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift
@@ -0,0 +1,528 @@
+import Foundation
+import UIKit
+import AsyncDisplayKit
+import Display
+import ComponentFlow
+import Postbox
+import TelegramCore
+import TelegramPresentationData
+import TelegramUIPreferences
+import AccountContext
+import AppBundle
+import AvatarNode
+import Markdown
+import GiftItemComponent
+import ChatMessagePaymentAlertController
+import ActivityIndicator
+import TooltipUI
+import MultilineTextComponent
+import BalancedTextComponent
+import TelegramStringFormatting
+
+private final class GiftOfferAlertContentNode: AlertContentNode {
+ private let context: AccountContext
+ private let strings: PresentationStrings
+ private var presentationTheme: PresentationTheme
+ private let title: String
+ private let text: String
+ private let amount: CurrencyAmount
+ private let gift: StarGift.UniqueGift
+
+ private let titleNode: ASTextNode
+ private let giftView = ComponentView()
+ private let textNode: ASTextNode
+ private let arrowNode: ASImageNode
+ private let avatarNode: AvatarNode
+ private let tableView = ComponentView()
+ private let valueDelta = ComponentView()
+
+ private let modelButtonTag = GenericComponentViewTag()
+ private let backdropButtonTag = GenericComponentViewTag()
+ private let symbolButtonTag = GenericComponentViewTag()
+
+ fileprivate var getController: () -> ViewController? = { return nil}
+
+ private let actionNodesSeparator: ASDisplayNode
+ private let actionNodes: [TextAlertContentActionNode]
+ private let actionVerticalSeparators: [ASDisplayNode]
+
+ private var activityIndicator: ActivityIndicator?
+
+ private var validLayout: CGSize?
+
+ var inProgress = false {
+ didSet {
+ if let size = self.validLayout {
+ let _ = self.updateLayout(size: size, transition: .immediate)
+ }
+ }
+ }
+
+ override var dismissOnOutsideTap: Bool {
+ return self.isUserInteractionEnabled
+ }
+
+ init(
+ context: AccountContext,
+ theme: AlertControllerTheme,
+ ptheme: PresentationTheme,
+ strings: PresentationStrings,
+ gift: StarGift.UniqueGift,
+ peer: EnginePeer,
+ title: String,
+ text: String,
+ amount: CurrencyAmount,
+ actions: [TextAlertAction]
+ ) {
+ self.context = context
+ self.strings = strings
+ self.presentationTheme = ptheme
+ self.title = title
+ self.text = text
+ self.amount = amount
+ self.gift = gift
+
+ self.titleNode = ASTextNode()
+ self.titleNode.maximumNumberOfLines = 0
+
+ self.textNode = ASTextNode()
+ self.textNode.maximumNumberOfLines = 0
+
+ self.arrowNode = ASImageNode()
+ self.arrowNode.displaysAsynchronously = false
+ self.arrowNode.displayWithoutProcessing = true
+
+ self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
+
+ self.actionNodesSeparator = ASDisplayNode()
+ self.actionNodesSeparator.isLayerBacked = true
+
+ self.actionNodes = actions.map { action -> TextAlertContentActionNode in
+ return TextAlertContentActionNode(theme: theme, action: action)
+ }
+
+ var actionVerticalSeparators: [ASDisplayNode] = []
+ if actions.count > 1 {
+ for _ in 0 ..< actions.count - 1 {
+ let separatorNode = ASDisplayNode()
+ separatorNode.isLayerBacked = true
+ actionVerticalSeparators.append(separatorNode)
+ }
+ }
+ self.actionVerticalSeparators = actionVerticalSeparators
+
+ super.init()
+
+ self.addSubnode(self.titleNode)
+ self.addSubnode(self.textNode)
+ self.addSubnode(self.arrowNode)
+ self.addSubnode(self.avatarNode)
+
+ self.addSubnode(self.actionNodesSeparator)
+
+ for actionNode in self.actionNodes {
+ self.addSubnode(actionNode)
+ }
+
+ for separatorNode in self.actionVerticalSeparators {
+ self.addSubnode(separatorNode)
+ }
+
+ self.updateTheme(theme)
+
+ self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
+ }
+
+ override func updateTheme(_ theme: AlertControllerTheme) {
+ self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
+ self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
+ link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
+ linkAttribute: { url in
+ return ("URL", url)
+ }
+ ), textAlignment: .center)
+ self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9))
+
+ self.actionNodesSeparator.backgroundColor = theme.separatorColor
+ for actionNode in self.actionNodes {
+ actionNode.updateTheme(theme)
+ }
+ for separatorNode in self.actionVerticalSeparators {
+ separatorNode.backgroundColor = theme.separatorColor
+ }
+
+ if let size = self.validLayout {
+ _ = self.updateLayout(size: size, transition: .immediate)
+ }
+ }
+
+ fileprivate func dismissAllTooltips() {
+ guard let controller = self.getController() else {
+ return
+ }
+ controller.window?.forEachController({ controller in
+ if let controller = controller as? TooltipScreen {
+ controller.dismiss(inPlace: false)
+ }
+ })
+ controller.forEachController({ controller in
+ if let controller = controller as? TooltipScreen {
+ controller.dismiss(inPlace: false)
+ }
+ return true
+ })
+ }
+
+ func showAttributeInfo(tag: Any, text: String) {
+ guard let controller = self.getController() else {
+ return
+ }
+ self.dismissAllTooltips()
+
+ guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else {
+ return
+ }
+
+ let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
+ let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
+ return .dismiss(consume: false)
+ })
+ controller.present(tooltipController, in: .current)
+ }
+
+ override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
+ var size = size
+ size.width = min(size.width, 310.0)
+
+ let strings = self.strings
+
+ self.validLayout = size
+
+ var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
+
+ let avatarSize = CGSize(width: 60.0, height: 60.0)
+ self.avatarNode.updateSize(size: avatarSize)
+
+ let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
+
+ let _ = self.giftView.update(
+ transition: .immediate,
+ component: AnyComponent(
+ GiftItemComponent(
+ context: self.context,
+ theme: self.presentationTheme,
+ strings: strings,
+ peer: nil,
+ subject: .uniqueGift(gift: self.gift, price: nil),
+ mode: .thumbnail
+ )
+ ),
+ environment: {},
+ containerSize: avatarSize
+ )
+ if let view = self.giftView.view {
+ if view.superview == nil {
+ self.view.addSubview(view)
+ }
+ view.frame = giftFrame
+ }
+
+ if let arrowImage = self.arrowNode.image {
+ let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
+ transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
+ }
+
+ let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
+ transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
+
+ origin.y += avatarSize.height + 17.0
+
+ let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
+ transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
+ origin.y += titleSize.height + 5.0
+
+ let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
+ transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
+ origin.y += textSize.height + 10.0
+
+ let actionButtonHeight: CGFloat = 44.0
+ var minActionsWidth: CGFloat = 0.0
+ let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
+ let actionTitleInsets: CGFloat = 8.0
+
+ for actionNode in self.actionNodes {
+ let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
+ minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
+ }
+
+ let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
+
+ let contentWidth = max(size.width, minActionsWidth)
+
+ let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
+
+ let tableFont = Font.regular(15.0)
+ let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor
+
+ var tableItems: [TableComponent.Item] = []
+ let order: [StarGift.UniqueGift.Attribute.AttributeType] = [
+ .model, .pattern, .backdrop, .originalInfo
+ ]
+
+ var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:]
+ for attribute in self.gift.attributes {
+ attributeMap[attribute.attributeType] = attribute
+ }
+
+ for type in order {
+ if let attribute = attributeMap[type] {
+ let id: String?
+ let title: String?
+ let value: NSAttributedString
+ let percentage: Float?
+ let tag: AnyObject?
+
+ switch attribute {
+ case let .model(name, _, rarity):
+ id = "model"
+ title = strings.Gift_Unique_Model
+ value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
+ percentage = Float(rarity) * 0.1
+ tag = self.modelButtonTag
+ case let .backdrop(name, _, _, _, _, _, rarity):
+ id = "backdrop"
+ title = strings.Gift_Unique_Backdrop
+ value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
+ percentage = Float(rarity) * 0.1
+ tag = self.backdropButtonTag
+ case let .pattern(name, _, rarity):
+ id = "pattern"
+ title = strings.Gift_Unique_Symbol
+ value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
+ percentage = Float(rarity) * 0.1
+ tag = self.symbolButtonTag
+ case .originalInfo:
+ continue
+ }
+
+ var items: [AnyComponentWithIdentity] = []
+ items.append(
+ AnyComponentWithIdentity(
+ id: AnyHashable(0),
+ component: AnyComponent(
+ MultilineTextComponent(text: .plain(value))
+ )
+ )
+ )
+ if let percentage, let tag {
+ items.append(AnyComponentWithIdentity(
+ id: AnyHashable(1),
+ component: AnyComponent(Button(
+ content: AnyComponent(ButtonContentComponent(
+ context: self.context,
+ text: formatPercentage(percentage),
+ color: self.presentationTheme.list.itemAccentColor
+ )),
+ action: { [weak self] in
+ self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string)
+ }
+ ).tagged(tag))
+ ))
+ }
+ let itemComponent = AnyComponent(
+ HStack(items, spacing: 4.0)
+ )
+
+ tableItems.append(.init(
+ id: id,
+ title: title,
+ hasBackground: false,
+ component: itemComponent
+ ))
+ }
+ }
+
+ let tableSize = self.tableView.update(
+ transition: .immediate,
+ component: AnyComponent(
+ TableComponent(
+ theme: self.presentationTheme,
+ items: tableItems,
+ semiTransparent: true
+ )
+ ),
+ environment: {},
+ containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
+ )
+ let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize)
+ if let view = self.tableView.view {
+ if view.superview == nil {
+ self.view.addSubview(view)
+ }
+ view.frame = tableFrame
+ }
+
+ var valueDeltaHeight: CGFloat = 0.0
+ if let valueAmount = self.gift.valueUsdAmount {
+ let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
+
+ let usdRate: Double
+ switch self.amount.currency {
+ case .stars:
+ usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0
+ case .ton:
+ usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0
+ }
+ let offerUsdValue = Double(self.amount.amount.value) * usdRate
+ let giftUsdValue = Double(valueAmount) / 100.0
+
+ let fraction = giftUsdValue / offerUsdValue
+ let percentage = Int(fraction * 100) - 100
+
+ if percentage > 20 {
+ let textColor = self.presentationTheme.list.itemDestructiveColor
+ let markdownAttributes = MarkdownAttributes(
+ body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
+ bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
+ link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
+ linkAttribute: { url in
+ return ("URL", url)
+ }
+ )
+ let valueDeltaSize = self.valueDelta.update(
+ transition: .immediate,
+ component: AnyComponent(
+ BalancedTextComponent(
+ text: .markdown(text: strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string, attributes: markdownAttributes),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0
+ )
+ ),
+ environment: {},
+ containerSize: CGSize(width: contentWidth - 32.0, height: size.height)
+ )
+ let valueDeltaFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - valueDeltaSize.width) / 2.0), y: avatarSize.height + titleSize.height + textSize.height + 73.0 + tableSize.height), size: valueDeltaSize)
+ if let view = self.valueDelta.view {
+ if view.superview == nil {
+ self.view.addSubview(view)
+ }
+ view.frame = valueDeltaFrame
+ }
+ valueDeltaHeight += valueDeltaSize.height + 10.0
+ }
+ }
+
+ let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + valueDeltaHeight + 40.0 + insets.top + insets.bottom)
+ transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
+
+ var actionOffset: CGFloat = 0.0
+ var separatorIndex = -1
+ var nodeIndex = 0
+ for actionNode in self.actionNodes {
+ if separatorIndex >= 0 {
+ let separatorNode = self.actionVerticalSeparators[separatorIndex]
+ do {
+ transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
+ }
+ }
+ separatorIndex += 1
+
+ let currentActionWidth: CGFloat
+ do {
+ currentActionWidth = resultSize.width
+ }
+
+ let actionNodeFrame: CGRect
+ do {
+ actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
+ actionOffset += actionButtonHeight
+ }
+
+ transition.updateFrame(node: actionNode, frame: actionNodeFrame)
+
+ nodeIndex += 1
+ }
+
+ if self.inProgress {
+ let activityIndicator: ActivityIndicator
+ if let current = self.activityIndicator {
+ activityIndicator = current
+ } else {
+ activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
+ self.addSubnode(activityIndicator)
+ }
+
+ if let actionNode = self.actionNodes.first {
+ actionNode.isUserInteractionEnabled = false
+ actionNode.isHidden = false
+
+ let indicatorSize = CGSize(width: 22.0, height: 22.0)
+ transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
+ }
+ }
+
+ return resultSize
+ }
+}
+
+public func giftOfferAlertController(
+ context: AccountContext,
+ gift: StarGift.UniqueGift,
+ peer: EnginePeer,
+ amount: CurrencyAmount,
+ commit: @escaping () -> Void
+) -> AlertController {
+ let presentationData = context.sharedContext.currentPresentationData.with { $0 }
+ let strings = presentationData.strings
+
+ let title = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Title
+ let buttonText: String = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Confirm
+
+ let priceString: String
+ switch amount.currency {
+ case .stars:
+ priceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(Int32(clamping: amount.amount.value))
+ case .ton:
+ priceString = "\(amount.amount) TON"
+ }
+
+ let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
+ let finalPriceString: String
+ switch amount.currency {
+ case .stars:
+ let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0))
+ finalPriceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(starsValue)
+ case .ton:
+ let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0)
+ finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON"
+ }
+
+ let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))"
+ let text = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text(giftTitle, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), priceString, finalPriceString).string
+
+ var contentNode: GiftOfferAlertContentNode?
+ var dismissImpl: ((Bool) -> Void)?
+ let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in
+ contentNode?.inProgress = true
+ commit()
+ }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
+ dismissImpl?(true)
+ })]
+
+ contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, amount: amount, actions: actions)
+
+ let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false)
+ contentNode?.getController = { [weak controller] in
+ return controller
+ }
+ dismissImpl = { [weak controller] animated in
+ if animated {
+ controller?.dismissAnimated()
+ } else {
+ controller?.dismiss()
+ }
+ }
+ return controller
+}
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift
index 45585138f6..a58d8bf55c 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift
@@ -344,7 +344,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode {
id: "fiatValue",
title: strings.Gift_Unique_Value,
component: AnyComponent(
- MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
+ MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))
),
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
))
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift
index e02517851e..43a1b83264 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift
@@ -16,6 +16,7 @@ import ButtonComponent
import PlainButtonComponent
import GiftItemComponent
import AccountContext
+import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -62,8 +63,7 @@ private final class SheetContent: CombinedComponent {
}
static var body: Body {
- let closeButton = Child(Button.self)
-
+ let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self)
let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
@@ -79,7 +79,6 @@ private final class SheetContent: CombinedComponent {
let theme = environment.theme
let strings = environment.strings
- let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(17.0)
@@ -87,20 +86,29 @@ private final class SheetContent: CombinedComponent {
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
- var contentSize = CGSize(width: context.availableSize.width, height: 10.0)
+ var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let closeButton = closeButton.update(
- component: Button(
- content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)),
- action: { [weak component] in
- component?.dismiss()
+ component: GlassBarButtonComponent(
+ size: CGSize(width: 40.0, height: 40.0),
+ backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
+ isDark: theme.overallDarkAppearance,
+ state: .generic,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Close",
+ tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
+ )
+ )),
+ action: { _ in
+ component.dismiss()
}
),
- availableSize: CGSize(width: 100.0, height: 30.0),
+ availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
- .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0))
+ .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0))
)
let title = title.update(
@@ -234,6 +242,7 @@ private final class SheetContent: CombinedComponent {
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
+ style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
@@ -256,7 +265,7 @@ private final class SheetContent: CombinedComponent {
}
}
),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
+ availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
transition: context.transition
)
context.add(button
@@ -335,6 +344,7 @@ private final class SheetContainerComponent: CombinedComponent {
})
}
)),
+ style: .glass,
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift
new file mode 100644
index 0000000000..89a13a0c49
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift
@@ -0,0 +1,1434 @@
+import Foundation
+import UIKit
+import AsyncDisplayKit
+import TelegramPresentationData
+import ComponentFlow
+import AccountContext
+import ViewControllerComponent
+import TelegramCore
+import SwiftSignalKit
+import Display
+import MultilineTextComponent
+import MultilineTextWithEntitiesComponent
+import ButtonComponent
+import PlainButtonComponent
+import Markdown
+import BundleIconComponent
+import TextFormat
+import TelegramStringFormatting
+import GlassBarButtonComponent
+import GiftItemComponent
+import EdgeEffect
+import AnimatedTextComponent
+import SegmentControlComponent
+import GiftAnimationComponent
+import GlassBackgroundComponent
+
+private final class GiftUpgradePreviewScreenComponent: Component {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let gift: StarGift
+ let attributes: [StarGift.UniqueGift.Attribute]
+
+ init(
+ context: AccountContext,
+ gift: StarGift,
+ attributes: [StarGift.UniqueGift.Attribute]
+ ) {
+ self.context = context
+ self.gift = gift
+ self.attributes = attributes
+ }
+
+ static func ==(lhs: GiftUpgradePreviewScreenComponent, rhs: GiftUpgradePreviewScreenComponent) -> Bool {
+ return true
+ }
+
+ private struct ItemLayout: Equatable {
+ var containerSize: CGSize
+ var containerInset: CGFloat
+ var containerCornerRadius: CGFloat
+ var bottomInset: CGFloat
+ var topInset: CGFloat
+
+ init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
+ self.containerSize = containerSize
+ self.containerInset = containerInset
+ self.containerCornerRadius = containerCornerRadius
+ self.bottomInset = bottomInset
+ self.topInset = topInset
+ }
+ }
+
+ enum SelectedSection {
+ case models
+ case backdrops
+ case symbols
+ }
+
+ private final class ScrollView: UIScrollView {
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ return super.hitTest(point, with: event)
+ }
+ }
+
+ final class View: UIView, UIScrollViewDelegate {
+ private let dimView: UIView
+ private let containerView: UIView
+ private let backgroundLayer: SimpleLayer
+ private let navigationBarContainer: SparseContainerView
+ private let glassContainerView: GlassBackgroundContainerView
+ private let scrollView: ScrollView
+ private let scrollContentClippingView: SparseContainerView
+ private let scrollContentView: UIView
+
+ private let backgroundHandleView: UIImageView
+
+ private let header = ComponentView()
+ private let closeButton = ComponentView()
+ private let playbackButton = ComponentView()
+
+ private let title = ComponentView()
+ private let subtitle = ComponentView()
+
+ private var attributeInfos: [ComponentView] = []
+
+ private let topEdgeSolidView = UIView()
+ private let topEdgeEffectView: EdgeEffectView
+ private let segmentControl = ComponentView()
+ private let descriptionText = ComponentView()
+
+ private var giftItems: [AnyHashable: ComponentView] = [:]
+
+ private var selectedSection: SelectedSection = .models
+
+ private let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
+
+ private var isPlaying = true
+ private var showRandomizeTip = false
+ private var previewTimer: SwiftSignalKit.Timer?
+ private var previewModelIndex: Int = 0
+ private var previewBackdropIndex: Int = 0
+ private var previewSymbolIndex: Int = 0
+
+ private var previewModels: [StarGift.UniqueGift.Attribute] = []
+ private var previewBackdrops: [StarGift.UniqueGift.Attribute] = []
+ private var previewSymbols: [StarGift.UniqueGift.Attribute] = []
+
+ private var selectedModel: StarGift.UniqueGift.Attribute?
+ private var selectedBackdrop: StarGift.UniqueGift.Attribute?
+ private var selectedSymbol: StarGift.UniqueGift.Attribute?
+
+ private var modelCount: Int32 = 0
+ private var backdropCount: Int32 = 0
+ private var symbolCount: Int32 = 0
+
+ private var ignoreScrolling: Bool = false
+
+ private var component: GiftUpgradePreviewScreenComponent?
+ private weak var state: EmptyComponentState?
+ private var isUpdating: Bool = false
+ private var environment: ViewControllerComponentContainer.Environment?
+ private var itemLayout: ItemLayout?
+
+ override init(frame: CGRect) {
+ self.dimView = UIView()
+ self.containerView = UIView()
+
+ self.containerView.clipsToBounds = true
+ self.containerView.layer.cornerRadius = 40.0
+ self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
+
+ self.backgroundLayer = SimpleLayer()
+ self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+ self.backgroundLayer.cornerRadius = 40.0
+
+ self.backgroundHandleView = UIImageView()
+
+ self.navigationBarContainer = SparseContainerView()
+
+ self.topEdgeEffectView = EdgeEffectView()
+ self.topEdgeEffectView.alpha = 0.0
+
+ self.glassContainerView = GlassBackgroundContainerView()
+
+ self.scrollView = ScrollView()
+
+ self.scrollContentClippingView = SparseContainerView()
+ self.scrollContentClippingView.clipsToBounds = true
+
+ self.scrollContentView = UIView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.dimView)
+ self.addSubview(self.containerView)
+ self.containerView.layer.addSublayer(self.backgroundLayer)
+
+ self.scrollView.delaysContentTouches = true
+ self.scrollView.canCancelContentTouches = true
+ self.scrollView.clipsToBounds = false
+ if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
+ self.scrollView.contentInsetAdjustmentBehavior = .never
+ }
+ if #available(iOS 13.0, *) {
+ self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
+ }
+ self.scrollView.showsVerticalScrollIndicator = false
+ self.scrollView.showsHorizontalScrollIndicator = false
+ self.scrollView.alwaysBounceHorizontal = false
+ self.scrollView.alwaysBounceVertical = true
+ self.scrollView.scrollsToTop = false
+ self.scrollView.delegate = self
+ self.scrollView.clipsToBounds = true
+
+ self.containerView.addSubview(self.scrollContentClippingView)
+ self.scrollContentClippingView.addSubview(self.scrollView)
+
+ self.scrollView.addSubview(self.scrollContentView)
+
+ self.containerView.addSubview(self.navigationBarContainer)
+
+ self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ if !self.ignoreScrolling {
+ self.updateScrolling(transition: .immediate)
+ }
+ }
+
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ if !self.bounds.contains(point) {
+ return nil
+ }
+ if !self.backgroundLayer.frame.contains(point) {
+ return self.dimView
+ }
+
+ if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
+ return result
+ }
+ let result = super.hitTest(point, with: event)
+ return result
+ }
+
+ @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
+ if case .ended = recognizer.state {
+ guard let environment = self.environment, let controller = environment.controller() else {
+ return
+ }
+ controller.dismiss()
+ }
+ }
+
+ private func updateScrolling(transition: ComponentTransition) {
+ guard let itemLayout = self.itemLayout else {
+ return
+ }
+ var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
+ topOffset = max(0.0, topOffset)
+ transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
+
+ transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
+
+ var topOffsetFraction = self.scrollView.bounds.minY / 100.0
+ topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
+
+ self.topEdgeEffectView.alpha = max(0.0, min(1.0, self.scrollView.bounds.minY / 8.0))
+
+ let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width
+ let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0
+ let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius
+
+ let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction
+ let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction)
+ let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction
+
+ var containerTransform = CATransform3DIdentity
+ containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0)
+ containerTransform = CATransform3DScale(containerTransform, scale, scale, scale)
+ transition.setTransform(view: self.containerView, transform: containerTransform)
+ transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius)
+
+ self.updateItems(transition: transition)
+ }
+
+ func animateIn() {
+ self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
+ let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
+ self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
+ self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
+ self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
+ }
+
+ func animateOut(completion: @escaping () -> Void) {
+ let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
+
+ self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
+ self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
+ completion()
+ })
+ self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
+ self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
+ }
+
+ private func previewTimerTick() {
+ guard !self.previewModels.isEmpty else { return }
+ self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count
+
+ let previousSymbolIndex = self.previewSymbolIndex
+ var randomSymbolIndex = previousSymbolIndex
+ while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty {
+ randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count)
+ }
+ if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex }
+
+ let previousBackdropIndex = self.previewBackdropIndex
+ var randomBackdropIndex = previousBackdropIndex
+ while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty {
+ randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count)
+ }
+ if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex }
+
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ }
+
+ private func updateTimer() {
+ if self.isPlaying {
+ self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.previewTimerTick()
+ }, queue: Queue.mainQueue())
+ self.previewTimer?.start()
+ } else {
+ self.previewTimer?.invalidate()
+ self.previewTimer = nil
+ }
+ }
+
+ private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = []
+ private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute]) {
+ var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = []
+ switch self.selectedSection {
+ case .models:
+ let models = Array(attributes.filter({ attribute in
+ if case .model = attribute {
+ return true
+ } else {
+ return false
+ }
+ }))
+ for model in models {
+ effectiveGifts.append([model])
+ }
+ case .backdrops:
+ let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex]
+ let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex]
+ let backdrops = Array(attributes.filter({ attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }))
+ for backdrop in backdrops {
+ effectiveGifts.append([
+ selectedModel,
+ backdrop,
+ selectedSymbol
+ ])
+ }
+ case .symbols:
+ let selectedBackdrop = self.selectedBackdrop ?? self.previewBackdrops[self.previewBackdropIndex]
+ let symbols = Array(attributes.filter({ attribute in
+ if case .pattern = attribute {
+ return true
+ } else {
+ return false
+ }
+ }))
+ for symbol in symbols {
+ effectiveGifts.append([
+ selectedBackdrop,
+ symbol
+ ])
+ }
+ }
+ self.effectiveGifts = effectiveGifts
+ }
+
+ private func updateItems(transition: ComponentTransition) {
+ guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
+ return
+ }
+
+ let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
+
+ let sideInset: CGFloat = 16.0 + environment.safeInsets.left
+
+ let optionSpacing: CGFloat = 10.0
+ let optionWidth = (itemLayout.containerSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
+ let optionSize = CGSize(width: optionWidth, height: 126.0)
+
+ let topInset: CGFloat = 393.0
+
+ var validIds: [AnyHashable] = []
+ var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: optionSize)
+
+ for attributeList in self.effectiveGifts {
+ var isVisible = false
+ if visibleBounds.intersects(itemFrame) {
+ isVisible = true
+ }
+
+ var itemId = ""
+ var title = ""
+ var rarity: Int32 = 0
+
+ var modelAttribute: StarGift.UniqueGift.Attribute?
+ var backdropAttribute: StarGift.UniqueGift.Attribute?
+ var symbolAttribute: StarGift.UniqueGift.Attribute?
+
+ switch self.selectedSection {
+ case .models:
+ itemId += "models_"
+ case .backdrops:
+ itemId += "backdrops_"
+ case .symbols:
+ itemId += "symbols_"
+ }
+
+ var isSelected = false
+ for attribute in attributeList {
+ switch attribute {
+ case let .model(name, _, rarityValue):
+ itemId += name
+ if self.selectedSection == .models {
+ title = name
+ rarity = rarityValue
+ modelAttribute = attribute
+
+ isSelected = self.selectedModel == attribute
+ }
+ case let .backdrop(name, _, _, _, _, _, rarityValue):
+ itemId += name
+ if self.selectedSection == .backdrops {
+ title = name
+ rarity = rarityValue
+ backdropAttribute = attribute
+
+ isSelected = self.selectedBackdrop == attribute
+ }
+ case let .pattern(name, _, rarityValue):
+ itemId += name
+ if self.selectedSection == .symbols {
+ title = name
+ rarity = rarityValue
+ symbolAttribute = attribute
+
+ isSelected = self.selectedSymbol == attribute
+ }
+ default:
+ break
+ }
+ }
+
+ if isVisible {
+ validIds.append(itemId)
+
+ var itemTransition = transition
+ let visibleItem: ComponentView
+ if let current = self.giftItems[itemId] {
+ visibleItem = current
+ } else {
+ visibleItem = ComponentView()
+ if !transition.animation.isImmediate {
+ itemTransition = .immediate
+ }
+ self.giftItems[itemId] = visibleItem
+ }
+
+ let subject: GiftItemComponent.Subject = .preview(attributes: attributeList, rarity: rarity)
+ let _ = visibleItem.update(
+ transition: itemTransition,
+ component: AnyComponent(
+ PlainButtonComponent(
+ content: AnyComponent(
+ GiftItemComponent(
+ context: component.context,
+ theme: environment.theme,
+ strings: environment.strings,
+ peer: nil,
+ subject: subject,
+ title: title,
+ ribbon: nil,
+ isSelected: isSelected,
+ mode: .upgradePreview
+ )
+ ),
+ effectAlignment: .center,
+ action: { [weak self] in
+ guard let self, let state = self.state else {
+ return
+ }
+ if self.isPlaying {
+ self.isPlaying = false
+ self.showRandomizeTip = true
+ Queue.mainQueue().after(2.0) {
+ if self.showRandomizeTip {
+ self.showRandomizeTip = false
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ }
+ }
+ }
+
+ switch self.selectedSection {
+ case .models:
+ self.selectedModel = modelAttribute
+ case .backdrops:
+ self.selectedBackdrop = backdropAttribute
+ case .symbols:
+ self.selectedSymbol = symbolAttribute
+ }
+
+ state.updated(transition: .easeInOut(duration: 0.25))
+ },
+ animateAlpha: false
+ )
+ ),
+ environment: {},
+ containerSize: optionSize
+ )
+ if let itemView = visibleItem.view {
+ if itemView.superview == nil {
+ self.scrollContentView.addSubview(itemView)
+
+ if !transition.animation.isImmediate {
+ itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
+ itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
+ }
+ }
+ itemTransition.setFrame(view: itemView, frame: itemFrame)
+ }
+ }
+ itemFrame.origin.x += itemFrame.width + optionSpacing
+ if itemFrame.maxX > itemLayout.containerSize.width {
+ itemFrame.origin.x = sideInset
+ itemFrame.origin.y += optionSize.height + optionSpacing
+ }
+ }
+
+ var removeIds: [AnyHashable] = []
+ for (id, item) in self.giftItems {
+ if !validIds.contains(id) {
+ removeIds.append(id)
+ if let itemView = item.view {
+ if !transition.animation.isImmediate {
+ itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
+ itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
+ itemView.removeFromSuperview()
+ })
+ } else {
+ itemView.removeFromSuperview()
+ }
+ }
+ }
+ }
+ for id in removeIds {
+ self.giftItems.removeValue(forKey: id)
+ }
+ }
+
+ func update(component: GiftUpgradePreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.isUpdating = true
+ defer {
+ self.isUpdating = false
+ }
+
+ self.updateTimer()
+
+ let environment = environment[ViewControllerComponentContainer.Environment.self].value
+ let themeUpdated = self.environment?.theme !== environment.theme
+
+ let resetScrolling = self.scrollView.bounds.width != availableSize.width
+
+ let fillingSize: CGFloat
+ if case .regular = environment.metrics.widthClass {
+ fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0
+ } else {
+ fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0
+ }
+ let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5)
+ let sideInset: CGFloat = rawSideInset + 16.0
+
+ if self.component == nil {
+ var modelCount: Int32 = 0
+ var backdropCount: Int32 = 0
+ var symbolCount: Int32 = 0
+ for attribute in component.attributes {
+ switch attribute {
+ case .model:
+ modelCount += 1
+ case .backdrop:
+ backdropCount += 1
+ case .pattern:
+ symbolCount += 1
+ default:
+ break
+ }
+ }
+ self.modelCount = modelCount
+ self.backdropCount = backdropCount
+ self.symbolCount = symbolCount
+
+ let randomModels = Array(component.attributes.filter({ attribute in
+ if case .model = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled().prefix(15))
+ self.previewModels = randomModels
+
+ let randomBackdrops = Array(component.attributes.filter({ attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled())
+ self.previewBackdrops = randomBackdrops
+
+ let randomSymbols = Array(component.attributes.filter({ attribute in
+ if case .pattern = attribute {
+ return true
+ } else {
+ return false
+ }
+ }).shuffled().prefix(15))
+ self.previewSymbols = randomSymbols
+
+ self.updateEffectiveGifts(attributes: component.attributes)
+ }
+
+ self.component = component
+ self.state = state
+ self.environment = environment
+
+ let theme = environment.theme.withModalBlocksBackground()
+
+ if themeUpdated {
+ self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
+ self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor
+ }
+
+ transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
+
+ var buttonColor: UIColor = .white.withAlphaComponent(0.1)
+ var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
+
+ var attributes: [StarGift.UniqueGift.Attribute] = []
+ if !self.previewModels.isEmpty {
+ if self.isPlaying {
+ attributes.append(self.previewModels[self.previewModelIndex])
+ attributes.append(self.previewBackdrops[self.previewBackdropIndex])
+ attributes.append(self.previewSymbols[self.previewSymbolIndex])
+ } else {
+ if self.selectedModel == nil {
+ self.selectedModel = self.previewModels[self.previewModelIndex]
+ }
+ if self.selectedBackdrop == nil {
+ self.selectedBackdrop = self.previewBackdrops[self.previewBackdropIndex]
+ }
+ if self.selectedSymbol == nil {
+ self.selectedSymbol = self.previewSymbols[self.previewSymbolIndex]
+ }
+ if let model = self.selectedModel {
+ attributes.append(model)
+ }
+ if let backdrop = self.selectedBackdrop {
+ attributes.append(backdrop)
+ }
+ if let symbol = self.selectedSymbol {
+ attributes.append(symbol)
+ }
+ }
+ }
+
+ if let backdropAttribute = attributes.first(where: { attribute in
+ if case .backdrop = attribute {
+ return true
+ } else {
+ return false
+ }
+ }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute {
+ let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04)
+ let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89)
+ buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25)
+
+ secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
+ }
+
+ var contentHeight: CGFloat = 0.0
+ let headerSize = self.header.update(
+ transition: transition,
+ component: AnyComponent(GiftCompositionComponent(
+ context: component.context,
+ theme: environment.theme,
+ subject: .preview(attributes),
+ animationOffset: CGPoint(x: 0.0, y: 20.0),
+ animationScale: nil,
+ displayAnimationStars: false,
+ alwaysAnimateTransition: true,
+ revealedAttributes: Set(),
+ externalState: self.giftCompositionExternalState,
+ requestUpdate: { [weak state] transition in
+ state?.updated(transition: transition)
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width, height: 300.0),
+ )
+ let headerFrame = CGRect(origin: CGPoint(x: floor((fillingSize - headerSize.width) * 0.5), y: 0.0), size: headerSize)
+ if let headerView = self.header.view {
+ if headerView.superview == nil {
+ headerView.isUserInteractionEnabled = false
+ headerView.clipsToBounds = true
+ headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
+ headerView.layer.cornerRadius = 38.0
+ self.navigationBarContainer.addSubview(headerView)
+ }
+ transition.setFrame(view: headerView, frame: headerFrame)
+ }
+
+ contentHeight += headerSize.height
+
+ var titleText: String = ""
+ if case let .generic(gift) = component.gift {
+ titleText = gift.title ?? ""
+ }
+
+ let titleSize = self.title.update(
+ transition: transition,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: titleText, font: Font.semibold(20.0), textColor: .white))
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - titleSize.width) * 0.5), y: contentHeight - 124.0), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.navigationBarContainer.addSubview(titleView)
+ }
+ transition.setFrame(view: titleView, frame: titleFrame)
+ }
+
+ let subtitleSize = self.subtitle.update(
+ transition: .spring(duration: 0.2),
+ component: AnyComponent(AnimatedTextComponent(
+ font: Font.regular(14.0),
+ color: secondaryTextColor,
+ items: [
+ AnimatedTextComponent.Item(id: self.isPlaying ? "random" : "selected", content: .text(self.isPlaying ? "Random" : "Selected")),
+ AnimatedTextComponent.Item(id: "traits", content: .text(" Traits"))
+ ],
+ noDelay: true,
+ blur: true
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
+ )
+ let subtitleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - subtitleSize.width) * 0.5), y: contentHeight - 97.0), size: subtitleSize)
+ if let subtitleView = self.subtitle.view {
+ if subtitleView.superview == nil {
+ self.navigationBarContainer.addSubview(subtitleView)
+ }
+ transition.setFrame(view: subtitleView, frame: subtitleFrame)
+ }
+
+
+ let attributeSpacing: CGFloat = 10.0
+ let attributeWidth: CGFloat = floor((fillingSize - sideInset * 2.0 - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count))
+ let attributeHeight: CGFloat = 45.0
+
+ for i in 0 ..< attributes.count {
+ var attributeFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (attributeWidth + attributeSpacing), y: contentHeight - 60.0), size: CGSize(width: attributeWidth, height: attributeHeight))
+ if i == attributes.count - 1 {
+ attributeFrame.size.width = max(0.0, availableSize.width - sideInset - attributeFrame.minX)
+ }
+ let attributeInfo: ComponentView
+ if self.attributeInfos.count > i {
+ attributeInfo = self.attributeInfos[i]
+ } else {
+ attributeInfo = ComponentView()
+ self.attributeInfos.append(attributeInfo)
+ }
+ let attribute = attributes[i]
+ let _ = attributeInfo.update(
+ transition: transition,
+ component: AnyComponent(AttributeInfoComponent(
+ strings: environment.strings,
+ backgroundColor: buttonColor,
+ secondaryTextColor: secondaryTextColor,
+ attribute: attribute
+ )),
+ environment: {},
+ containerSize: attributeFrame.size
+ )
+ if let attributeInfoView = attributeInfo.view {
+ if attributeInfoView.superview == nil {
+ self.navigationBarContainer.addSubview(attributeInfoView)
+ }
+ transition.setFrame(view: attributeInfoView, frame: attributeFrame)
+ }
+ }
+
+ let edgeEffectHeight: CGFloat = 44.0
+ let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight + 44.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
+ let edgeSolidFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: 44.0))
+ transition.setFrame(view: self.topEdgeSolidView, frame: edgeSolidFrame)
+ transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
+ self.topEdgeSolidView.backgroundColor = theme.list.blocksBackgroundColor
+ self.topEdgeEffectView.update(content: theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
+
+ contentHeight += 16.0
+
+ let segmentedSize = self.segmentControl.update(
+ transition: transition,
+ component: AnyComponent(SegmentControlComponent(
+ theme: environment.theme,
+ items: [
+ SegmentControlComponent.Item(id: AnyHashable(SelectedSection.models), title: environment.strings.Gift_Variants_Models),
+ SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: environment.strings.Gift_Variants_Backdrops),
+ SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: environment.strings.Gift_Variants_Symbols)
+ ],
+ selectedId: "models",
+ action: { [weak self] id in
+ guard let self, let component = self.component, let id = id.base as? SelectedSection else {
+ return
+ }
+ self.selectedSection = id
+ self.isPlaying = false
+
+ self.updateEffectiveGifts(attributes: component.attributes)
+ self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
+ })),
+ environment: {},
+ containerSize: CGSize(width: fillingSize - 8.0 * 2.0, height: 100.0)
+ )
+ let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((fillingSize - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize)
+ if let segmentedControlComponentView = self.segmentControl.view {
+ if segmentedControlComponentView.superview == nil {
+ self.navigationBarContainer.addSubview(self.topEdgeSolidView)
+ self.navigationBarContainer.addSubview(self.topEdgeEffectView)
+ self.navigationBarContainer.addSubview(segmentedControlComponentView)
+ }
+ transition.setFrame(view: segmentedControlComponentView, frame: segmentedControlFrame)
+ }
+ contentHeight += segmentedSize.height
+ contentHeight += 18.0
+
+ let itemHeight: CGFloat = 126.0
+ let itemSpacing: CGFloat = 10.0
+
+ let descriptionText: String
+ let itemCount: Int32
+ switch self.selectedSection {
+ case .models:
+ descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.modelCount)).string
+ itemCount = self.modelCount
+ case .backdrops:
+ descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Backdrop(self.backdropCount)).string
+ itemCount = self.backdropCount
+ case .symbols:
+ descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Symbol(self.symbolCount)).string
+ itemCount = self.symbolCount
+ }
+
+ let descriptionFont = Font.regular(13.0)
+ let descriptionBoldFont = Font.semibold(13.0)
+ let descriptionTextColor = theme.list.itemSecondaryTextColor
+ let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), linkAttribute: { contents in
+ return (TelegramTextAttributes.URL, contents)
+ })
+
+ let descriptionSize = self.descriptionText.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes)
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
+ )
+ let descriptionFrame = CGRect(origin: CGPoint(x: floor((fillingSize - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize)
+ if let descriptionView = self.descriptionText.view {
+ if descriptionView.superview == nil {
+ self.scrollContentView.addSubview(descriptionView)
+ }
+ descriptionView.frame = descriptionFrame
+ }
+ contentHeight += descriptionSize.height
+ contentHeight += 26.0
+
+ contentHeight += (itemHeight + itemSpacing) * ceil(CGFloat(itemCount) / 3.0)
+
+ if self.backgroundHandleView.image == nil {
+ self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
+ }
+ self.backgroundHandleView.tintColor = UIColor.white.withAlphaComponent(0.4)
+ let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0))
+ if self.backgroundHandleView.superview == nil {
+ self.navigationBarContainer.addSubview(self.backgroundHandleView)
+ }
+ transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame)
+
+ self.glassContainerView.update(size: CGSize(width: fillingSize, height: 64.0), isDark: false, transition: .immediate)
+ self.glassContainerView.frame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: 64.0))
+
+ let closeButtonSize = self.closeButton.update(
+ transition: transition,
+ component: AnyComponent(GlassBarButtonComponent(
+ size: CGSize(width: 40.0, height: 40.0),
+ backgroundColor: buttonColor,
+ isDark: false,
+ state: .tintedGlass,
+ component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
+ BundleIconComponent(
+ name: "Navigation/Back",
+ tintColor: .white
+ )
+ )),
+ action: { [weak self] _ in
+ guard let self else {
+ return
+ }
+ self.environment?.controller()?.dismiss()
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: 40.0, height: 40.0)
+ )
+ let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize)
+ if let closeButtonView = self.closeButton.view {
+ if closeButtonView.superview == nil {
+ self.navigationBarContainer.addSubview(self.glassContainerView)
+ self.navigationBarContainer.addSubview(closeButtonView)
+ }
+ transition.setFrame(view: closeButtonView, frame: closeButtonFrame)
+ }
+
+ let playbackButtonSize = self.playbackButton.update(
+ transition: transition,
+ component: AnyComponent(GlassBarButtonComponent(
+ size: nil,
+ backgroundColor: buttonColor,
+ isDark: false,
+ state: .tintedGlass,
+ component: AnyComponentWithIdentity(id: "content", component: AnyComponent(
+ PlayButtonComponent(isPlay: !self.isPlaying, title: !self.isPlaying && self.showRandomizeTip ? environment.strings.Gift_Variants_Randomize : nil)
+ )),
+ action: { [weak self] _ in
+ guard let self else {
+ return
+ }
+ self.isPlaying = !self.isPlaying
+
+ if !self.isPlaying {
+ self.showRandomizeTip = true
+ Queue.mainQueue().after(2.0) {
+ if self.showRandomizeTip {
+ self.showRandomizeTip = false
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ }
+ }
+ } else {
+ self.selectedModel = nil
+ self.selectedBackdrop = nil
+ self.selectedSymbol = nil
+
+ self.showRandomizeTip = false
+
+ self.previewTimerTick()
+ }
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: 160.0, height: 40.0)
+ )
+ let playbackButtonFrame = CGRect(origin: CGPoint(x: fillingSize - 16.0 - playbackButtonSize.width, y: 16.0), size: playbackButtonSize)
+ if let playbackButtonView = self.playbackButton.view {
+ if playbackButtonView.superview == nil {
+ self.glassContainerView.contentView.addSubview(playbackButtonView)
+ }
+ transition.setFrame(view: playbackButtonView, frame: playbackButtonFrame)
+ }
+
+ let containerInset: CGFloat = environment.statusBarHeight + 10.0
+ contentHeight += environment.safeInsets.bottom
+
+ var initialContentHeight = contentHeight
+ let clippingY: CGFloat
+
+ initialContentHeight = contentHeight
+
+ clippingY = availableSize.height
+
+ let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
+
+ let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
+
+ self.scrollContentClippingView.layer.cornerRadius = 38.0
+
+ self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset)
+
+ transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
+
+ transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
+ transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height)))
+
+ let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset))
+ transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
+ transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
+
+ self.ignoreScrolling = true
+ transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
+ let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
+ if contentSize != self.scrollView.contentSize {
+ self.scrollView.contentSize = contentSize
+ }
+ if resetScrolling {
+ self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
+ }
+ self.ignoreScrolling = false
+ self.updateScrolling(transition: transition)
+
+ transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
+ transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
+
+ if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
+ let bottomInset: CGFloat = contentHeight - 12.0
+
+ let layout = ContainerViewLayout(
+ size: availableSize,
+ metrics: environment.metrics,
+ deviceMetrics: environment.deviceMetrics,
+ intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0),
+ safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0),
+ additionalInsets: .zero,
+ statusBarHeight: environment.statusBarHeight,
+ inputHeight: nil,
+ inputHeightIsInteractivellyChanging: false,
+ inVoiceOver: false
+ )
+ controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
+ }
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+public class GiftUpgradePreviewScreen: ViewControllerComponentContainer {
+ private let context: AccountContext
+
+ private var didPlayAppearAnimation: Bool = false
+ private var isDismissed: Bool = false
+
+ public init(
+ context: AccountContext,
+ gift: StarGift,
+ attributes: [StarGift.UniqueGift.Attribute]
+ ) {
+ self.context = context
+
+ super.init(context: context, component: GiftUpgradePreviewScreenComponent(
+ context: context,
+ gift: gift,
+ attributes: attributes
+ ), navigationBarAppearance: .none, theme: .default)
+
+ self.statusBar.statusBarStyle = .Ignore
+ self.navigationPresentation = .flatModal
+ self.blocksBackgroundWhenInOverlay = true
+ self.automaticallyControlPresentationContextLayout = false
+ }
+
+ required public init(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ deinit {
+ }
+
+ override public func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ self.view.disablesInteractiveModalDismiss = true
+
+ if !self.didPlayAppearAnimation {
+ self.didPlayAppearAnimation = true
+
+ if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View {
+ componentView.animateIn()
+ }
+ }
+ }
+
+ override public func dismiss(completion: (() -> Void)? = nil) {
+ if !self.isDismissed {
+ self.isDismissed = true
+
+ if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View {
+ componentView.animateOut(completion: { [weak self] in
+ completion?()
+ self?.dismiss(animated: false)
+ })
+ } else {
+ self.dismiss(animated: false)
+ }
+ }
+ }
+}
+
+private final class AttributeInfoComponent: Component {
+ let strings: PresentationStrings
+ let backgroundColor: UIColor
+ let secondaryTextColor: UIColor
+ let attribute: StarGift.UniqueGift.Attribute
+
+ init(
+ strings: PresentationStrings,
+ backgroundColor: UIColor,
+ secondaryTextColor: UIColor,
+ attribute: StarGift.UniqueGift.Attribute
+ ) {
+ self.strings = strings
+ self.backgroundColor = backgroundColor
+ self.secondaryTextColor = secondaryTextColor
+ self.attribute = attribute
+ }
+
+ static func ==(lhs: AttributeInfoComponent, rhs: AttributeInfoComponent) -> Bool {
+ if lhs.strings !== rhs.strings {
+ return false
+ }
+ if lhs.backgroundColor != rhs.backgroundColor {
+ return false
+ }
+ if lhs.secondaryTextColor != rhs.secondaryTextColor {
+ return false
+ }
+ if lhs.attribute != rhs.attribute {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ let background = SimpleLayer()
+ let title = ComponentView()
+ let subtitle = ComponentView()
+
+ let badgeBackground = SimpleLayer()
+ let badge = ComponentView()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(component: AttributeInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ let backgroundFrame = CGRect(origin: CGPoint(), size: availableSize)
+ if self.background.superlayer == nil {
+ self.background.cornerRadius = 16.0
+ self.background.cornerCurve = .continuous
+ self.layer.addSublayer(self.background)
+
+ self.badgeBackground.cornerRadius = 9.5
+ self.badgeBackground.cornerCurve = .continuous
+ self.layer.addSublayer(self.badgeBackground)
+ }
+ self.background.frame = backgroundFrame
+ transition.setBackgroundColor(layer: self.background, color: component.backgroundColor)
+
+ let title: String
+ let subtitle: String
+ let rarity: Int32
+ switch component.attribute {
+ case let .model(name, _, rarityValue):
+ title = name
+ subtitle = component.strings.Gift_Variants_Model
+ rarity = rarityValue
+ case let .backdrop(name, _, _, _, _, _, rarityValue):
+ title = name
+ subtitle = component.strings.Gift_Variants_Backdrop
+ rarity = rarityValue
+ case let .pattern(name, _, rarityValue):
+ title = name
+ subtitle = component.strings.Gift_Variants_Symbol
+ rarity = rarityValue
+ default:
+ title = ""
+ subtitle = ""
+ rarity = 0
+ }
+
+ let _ = rarity
+
+ let titleSize = self.title.update(
+ transition: .spring(duration: 0.2),
+ component: AnyComponent(AnimatedTextComponent(
+ font: Font.semibold(13.0),
+ color: UIColor.white,
+ items: [AnimatedTextComponent.Item(id: "title", content: .text(title))],
+ noDelay: true,
+ blur: true
+ )),
+ environment: {},
+ containerSize: CGSize(width: backgroundFrame.size.width - 8.0, height: backgroundFrame.size.height)
+ )
+ let subtitleSize = self.subtitle.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: subtitle, font: Font.regular(11.0), textColor: .white)),
+ tintColor: component.secondaryTextColor
+ )),
+ environment: {},
+ containerSize: backgroundFrame.size
+ )
+
+ let spacing: CGFloat = 0.0
+ let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleSize.width) * 0.5), y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize)
+ let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize)
+
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.addSubview(titleView)
+ }
+ transition.setFrame(view: titleView, frame: titleFrame)
+ }
+
+ if let subtitleView = self.subtitle.view {
+ if subtitleView.superview == nil {
+ self.addSubview(subtitleView)
+ }
+ transition.setFrame(view: subtitleView, frame: subtitleFrame)
+ }
+
+ func formatPercentage(_ value: Float) -> String {
+ return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "")
+ }
+ let percentage = Float(rarity) * 0.1
+
+ let badgeSize = self.badge.update(
+ transition: .spring(duration: 0.2),
+ component: AnyComponent(AnimatedTextComponent(
+ font: Font.with(size: 12.0, weight: .semibold, traits: .monospacedNumbers),
+ color: UIColor.white,
+ items: [
+ AnimatedTextComponent.Item(id: "value", content: .text(formatPercentage(percentage))),
+ AnimatedTextComponent.Item(id: "percent", content: .text("%")),
+ ],
+ noDelay: true,
+ blur: true
+ )),
+ environment: {},
+ containerSize: backgroundFrame.size
+ )
+ let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeSize.width - 2.0, y: backgroundFrame.minY - 8.0), size: badgeSize)
+ if let badgeView = self.badge.view {
+ if badgeView.superview == nil {
+ self.addSubview(badgeView)
+ }
+ transition.setFrame(view: badgeView, frame: badgeFrame)
+ }
+
+ let badgeBackgroundFrame = badgeFrame.insetBy(dx: -5.5, dy: -2.0)
+ transition.setFrame(layer: self.badgeBackground, frame: badgeBackgroundFrame)
+ transition.setBackgroundColor(layer: self.badgeBackground, color: component.backgroundColor.mixedWith(component.secondaryTextColor, alpha: 0.5))
+
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+
+private final class PlayButtonComponent: Component {
+ let isPlay: Bool
+ let title: String?
+
+ public init(
+ isPlay: Bool,
+ title: String?
+ ) {
+ self.isPlay = isPlay
+ self.title = title
+ }
+
+ static func ==(lhs: PlayButtonComponent, rhs: PlayButtonComponent) -> Bool {
+ if lhs.isPlay != rhs.isPlay {
+ return false
+ }
+ if lhs.title != rhs.title {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ private var component: PlayButtonComponent?
+ private weak var componentState: EmptyComponentState?
+
+ private let containerView = UIView()
+ private let titleContainerView = UIView()
+ private let title = ComponentView()
+ private let play = ComponentView()
+ private let pause = ComponentView()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ self.containerView.clipsToBounds = true
+ self.containerView.layer.cornerRadius = 20.0
+ self.addSubview(self.containerView)
+
+ self.titleContainerView.clipsToBounds = true
+ self.containerView.addSubview(self.titleContainerView)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(component: PlayButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+ self.componentState = state
+
+ var contentSize = CGSize(width: 15.0, height: 21.0)
+
+ var titleSize = CGSize()
+ if let titleString = component.title {
+ titleSize = self.title.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: .white)))),
+ environment: {},
+ containerSize: availableSize
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: 9.0, y: 10.0), size: titleSize)
+ if let titleView = self.title.view {
+ titleView.alpha = 1.0
+ if titleView.superview == nil {
+ self.titleContainerView.addSubview(titleView)
+ transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
+ }
+ titleView.frame = titleFrame
+ }
+ contentSize.width += titleSize.width + 4.0
+ } else if let titleView = self.title.view {
+ transition.setAlpha(view: titleView, alpha: 0.0, completion: { finished in
+ if finished {
+ titleView.removeFromSuperview()
+ }
+ })
+ }
+ transition.setFrame(view: self.titleContainerView, frame: CGRect(origin: .zero, size: CGSize(width: titleSize.width + 14.0, height: 40.0)))
+
+ if component.isPlay {
+ let iconSize = self.play.update(
+ transition: .immediate,
+ component: AnyComponent(BundleIconComponent(name: "Media Gallery/PlayButton", tintColor: .white)),
+ environment: {},
+ containerSize: availableSize
+ )
+ let iconFrame = CGRect(origin: CGPoint(x: contentSize.width - iconSize.width + 21.0, y: 5.0), size: iconSize)
+ if let iconView = self.play.view {
+ iconView.alpha = 1.0
+ if iconView.superview == nil {
+ self.containerView.addSubview(iconView)
+ transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
+ transition.animateScale(view: iconView, from: 0.01, to: 1.0)
+ }
+ transition.setFrame(view: iconView, frame: iconFrame)
+ }
+ } else if let iconView = self.play.view {
+ transition.setAlpha(view: iconView, alpha: 0.0, completion: { finished in
+ if finished {
+ iconView.removeFromSuperview()
+ }
+ })
+ transition.animateScale(view: iconView, from: 1.0, to: 0.01)
+ }
+
+ if !component.isPlay {
+ let iconSize = self.pause.update(
+ transition: .immediate,
+ component: AnyComponent(BundleIconComponent(name: "Media Gallery/PictureInPicturePause", tintColor: .white)),
+ environment: {},
+ containerSize: availableSize
+ )
+ let iconFrame = CGRect(origin: CGPoint(x: contentSize.width - iconSize.width + 12.0 - UIScreenPixel, y: 13.0 - UIScreenPixel), size: iconSize)
+ if let iconView = self.pause.view {
+ iconView.alpha = 1.0
+ if iconView.superview == nil {
+ self.containerView.addSubview(iconView)
+ transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
+ transition.animateScale(view: iconView, from: 0.01, to: 1.0)
+ }
+ transition.setFrame(view: iconView, frame: iconFrame)
+ }
+ } else if let iconView = self.pause.view {
+ transition.setAlpha(view: iconView, alpha: 0.0, completion: { finished in
+ if finished {
+ iconView.removeFromSuperview()
+ }
+ })
+ transition.animateScale(view: iconView, from: 1.0, to: 0.01)
+ }
+
+ let containerWidth: CGFloat = contentSize.width + 26.0
+ let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - containerWidth) / 2.0), y: floorToScreenPixels((contentSize.height - 40.0) / 2.0)), size: CGSize(width: containerWidth, height: 40.0))
+ transition.setFrame(view: self.containerView, frame: containerFrame)
+
+ return contentSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift
index 2067859721..5586711858 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift
@@ -370,7 +370,7 @@ private final class GiftValueSheetContent: CombinedComponent {
)
))
- let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (≈\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
+ let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))"
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift
index 3dd6d8ced1..514b601653 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift
@@ -39,6 +39,7 @@ import GiftAnimationComponent
import ChatThemeScreen
import ProfileLevelRatingBarComponent
import AnimatedTextComponent
+import InfoParagraphComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -720,8 +721,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let dropOriginalDetailsImpl {
signal = dropOriginalDetailsImpl(reference)
} else {
- signal = (context.engine.payments.dropStarGiftOriginalDetails(reference: reference)
- |> deliverOnMainQueue)
+ signal = context.engine.payments.dropStarGiftOriginalDetails(reference: reference)
}
self.upgradeDisposable = (signal
@@ -738,7 +738,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes))))
case let .message(message):
- if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned) = action.action, case let .unique(uniqueGift) = gift {
+ if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift {
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
let updatedMedia: [Media] = [
TelegramMediaAction(
@@ -758,7 +758,9 @@ private final class GiftViewSheetContent: CombinedComponent {
canTransferDate: canTransferDate,
canResaleDate: canResaleDate,
dropOriginalDetailsStars: nil,
- assigned: assigned)
+ assigned: assigned,
+ fromOffer: fromOffer
+ )
)
]
@@ -1323,7 +1325,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
- func viewUpgradedGift(messageId: EngineMessage.Id) {
+ func viewUpgradedGift(messageId: EngineMessage.Id, delay: Bool) {
guard let controller = self.getController(), let navigationController = controller.navigationController as? NavigationController else {
return
}
@@ -1334,7 +1336,17 @@ private final class GiftViewSheetContent: CombinedComponent {
guard let self, let navigationController, let peer else {
return
}
- self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, purposefulAction: {}, peekData: nil, forceAnimatedScroll: true))
+ let action = {
+ self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, purposefulAction: {}, peekData: nil, forceAnimatedScroll: true))
+ }
+
+ if delay {
+ Queue.mainQueue().after(0.3) {
+ action()
+ }
+ } else {
+ action()
+ }
})
}
@@ -1425,6 +1437,16 @@ private final class GiftViewSheetContent: CombinedComponent {
self?.shareGift()
})))
+
+ if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId, uniqueGift.minOfferStars != nil {
+ items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_BuyOffer, icon: { theme in
+ return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor)
+ }, action: { [weak self] c, _ in
+ c?.dismiss(completion: nil)
+
+ self?.openGiftBuyOffer()
+ })))
+ }
if gift.flags.contains(.isThemeAvailable) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_SetAsTheme, icon: { theme in
@@ -2310,6 +2332,118 @@ private final class GiftViewSheetContent: CombinedComponent {
controller.switchToNextUpgradable()
}
}
+
+ func openGiftBuyOffer() {
+ guard let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, case let .peerId(ownerPeerId) = uniqueGift.owner, let controller = self.getController() else {
+ return
+ }
+ let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: ownerPeerId))
+ |> deliverOnMainQueue).start(next: { [weak self] peer in
+ guard let self, let peer else {
+ return
+ }
+ let buyController = self.context.sharedContext.makeStarsWithdrawalScreen(context: self.context, subject: .starGiftOffer(peer: peer, gift: uniqueGift, completion: { [weak self] amount, duration in
+ guard let self else {
+ return
+ }
+ self.commitGiftBuyOffer(peer: peer, price: amount, duration: duration)
+ }))
+ controller.push(buyController)
+ })
+ }
+
+ func commitGiftBuyOffer(peer: EnginePeer, price: CurrencyAmount, duration: Int32) {
+ guard let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
+ return
+ }
+
+ let context = self.context
+ let proceed = { [weak self, weak starsContext] in
+ guard let self else {
+ return
+ }
+ self.upgradeDisposable = (context.engine.payments.sendStarGiftOffer(peerId: peer.id, slug: uniqueGift.slug, amount: price, duration: duration, allowPaidStars: nil)
+ |> deliverOnMainQueue).start(error: { _ in
+ }, completed: { [weak self, weak starsContext] in
+ guard let self else {
+ return
+ }
+ Queue.mainQueue().after(0.5) {
+ starsContext?.load(force: true)
+ }
+ self.openPeer(peer, dismiss: true)
+ })
+ }
+
+ if price.currency == .stars, starsState.balance < price.amount {
+ let _ = (self.starsTopUpOptionsPromise.get()
+ |> filter { $0 != nil }
+ |> take(1)
+ |> deliverOnMainQueue).startStandalone(next: { [weak self] options in
+ guard let self, let controller = self.getController() else {
+ return
+ }
+ let purchaseController = context.sharedContext.makeStarsPurchaseScreen(
+ context: context,
+ starsContext: starsContext,
+ options: options ?? [],
+ purpose: .starGiftOffer(requiredStars: price.amount.value),
+ targetPeerId: nil,
+ customTheme: nil,
+ completion: { [weak self, weak starsContext] stars in
+ guard let self, let starsContext else {
+ return
+ }
+ self.inProgress = true
+ self.updated()
+
+ starsContext.add(balance: StarsAmount(value: stars, nanos: 0))
+ let _ = (starsContext.onUpdate
+ |> deliverOnMainQueue).start(next: { [weak self] in
+ guard let self else {
+ return
+ }
+ Queue.mainQueue().after(0.1, { [weak self] in
+ guard let self, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else {
+ return
+ }
+ if starsState.balance < price.amount {
+ self.inProgress = false
+ self.updated()
+
+ self.commitGiftBuyOffer(peer: peer, price: price, duration: duration)
+ } else {
+ proceed()
+ }
+ });
+ })
+ }
+ )
+ controller.push(purchaseController)
+ })
+ } else if price.currency == .ton, let tonState = context.tonContext?.currentState, tonState.balance < price.amount {
+ guard let controller = self.getController() else {
+ return
+ }
+ let needed = price.amount - tonState.balance
+ var fragmentUrl = "https://fragment.com/ads/topup"
+ if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String {
+ fragmentUrl = value
+ }
+ controller.push(BalanceNeededScreen(
+ context: self.context,
+ amount: needed,
+ buttonAction: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.context.sharedContext.applicationBindings.openUrl(fragmentUrl)
+ }
+ ))
+ } else {
+ proceed()
+ }
+ }
}
func makeState() -> State {
@@ -2334,7 +2468,6 @@ private final class GiftViewSheetContent: CombinedComponent {
let wearAvatar = Child(AvatarComponent.self)
let wearPeerName = Child(MultilineTextComponent.self)
- let wearPeerStatus = Child(MultilineTextComponent.self)
let wearTitle = Child(MultilineTextComponent.self)
let wearDescription = Child(MultilineTextComponent.self)
let wearPerks = Child(List.self)
@@ -2391,6 +2524,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var exported = false
var canUpgrade = false
var upgradeStars: Int64?
+ var genericGift: StarGift.Gift?
var uniqueGift: StarGift.UniqueGift?
var isSelfGift = false
var isChannelGift = false
@@ -2411,7 +2545,7 @@ private final class GiftViewSheetContent: CombinedComponent {
titleString = strings.Gift_View_UnavailableTitle
} else if let arguments = subject.arguments {
if let toPeerId = arguments.auctionToPeerId {
- isSelfGift = arguments.messageId?.peerId.isTelegramNotifications == true && toPeerId == component.context.account.peerId
+ isSelfGift = arguments.messageId?.peerId.isTelegramNotifications == true && toPeerId == component.context.account.peerId
} else {
isSelfGift = arguments.messageId?.peerId == component.context.account.peerId
}
@@ -2421,7 +2555,7 @@ private final class GiftViewSheetContent: CombinedComponent {
subtitleString = strings.Gift_View_ReleasedBy("[@\(addressName)]()").string
releasedByPeer = peer
}
-
+ genericGift = gift
animationFile = gift.file
stars = gift.price
text = arguments.text
@@ -2469,7 +2603,9 @@ private final class GiftViewSheetContent: CombinedComponent {
isMyOwnedUniqueGift = true
}
- if isSelfGift {
+ if let number = arguments.giftNumber, let title = genericGift?.title {
+ titleString = "\(title) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))"
+ } else if isSelfGift {
titleString = strings.Gift_View_Self_Title
} else {
titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title
@@ -2556,6 +2692,9 @@ private final class GiftViewSheetContent: CombinedComponent {
} else if case let .upgradePreview(attributes, _) = component.subject {
headerHeight = 258.0
headerSubject = .preview(attributes)
+ } else if case let .wearPreview(_, attributes) = component.subject, let attributes {
+ headerHeight = 200.0
+ headerSubject = .preview(attributes)
} else if let animationFile {
headerHeight = 210.0
headerSubject = .generic(animationFile)
@@ -2575,7 +2714,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
var wearPeerNameChild: _UpdatedChildComponent?
- if showWearPreview, let uniqueGift {
+ if showWearPreview {
var peerName = ""
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
@@ -2584,7 +2723,7 @@ private final class GiftViewSheetContent: CombinedComponent {
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: peerName,
- font: Font.bold(28.0),
+ font: Font.bold(20.0),
textColor: .white,
paragraphAlignment: .center
)),
@@ -2596,10 +2735,20 @@ private final class GiftViewSheetContent: CombinedComponent {
)
let giftTitle: String
- if case .wearPreview = component.subject {
- giftTitle = uniqueGift.title
+ if let uniqueGift {
+ if case .wearPreview = component.subject {
+ giftTitle = uniqueGift.title
+ } else {
+ giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))"
+ }
+ } else if let genericGift {
+ if let number = component.subject.arguments?.giftNumber {
+ giftTitle = "\(genericGift.title ?? "") #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))"
+ } else {
+ giftTitle = genericGift.title ?? ""
+ }
} else {
- giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))"
+ giftTitle = ""
}
let wearTitle = wearTitle.update(
@@ -2632,14 +2781,14 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .immediate
)
- var titleOriginY = headerHeight + 18.0
+ var titleOriginY = headerHeight + 10.0
context.add(wearTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearTitle.size.height))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
titleOriginY += wearTitle.size.height
- titleOriginY += 18.0
+ titleOriginY += 10.0
context.add(wearDescription
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearDescription.size.height))
@@ -2651,7 +2800,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var animationOffset: CGPoint?
var animationScale: CGFloat?
if let wearPeerNameChild {
- animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 56.0)
+ animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 79.0)
animationScale = 0.19
}
@@ -2714,42 +2863,21 @@ private final class GiftViewSheetContent: CombinedComponent {
)
headerComponents.append({
context.add(wearAvatar
- .position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0))
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
})
}
-
- let wearPeerStatus = wearPeerStatus.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: isChannelGift ? strings.Channel_Status : strings.Presence_online,
- font: Font.regular(17.0),
- textColor: vibrantColor,
- paragraphAlignment: .center
- )),
- horizontalAlignment: .center,
- maximumNumberOfLines: 5,
- lineSpacing: 0.2
- ),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
- transition: .immediate
- )
-
+
headerComponents.append({
context.add(wearPeerNameChild
- .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0))
- .appear(.default(alpha: true))
- .disappear(.default(alpha: true))
- )
- context.add(wearPeerStatus
- .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0))
+ .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0))
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
})
- originY += 108.0
+ originY += 91.0
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
@@ -2759,7 +2887,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "badge",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Badge_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Badge_ChannelText : strings.Gift_Wear_Badge_Text,
@@ -2773,7 +2901,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "design",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Design_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Design_ChannelText : strings.Gift_Wear_Design_Text,
@@ -2787,7 +2915,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "proof",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Wear_Proof_Title,
titleColor: textColor,
text: isChannelGift ? strings.Gift_Wear_Proof_ChannelText : strings.Gift_Wear_Proof_Text,
@@ -2905,7 +3033,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "unique",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Unique_Title,
titleColor: textColor,
text: uniqueText,
@@ -2919,7 +3047,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "transferable",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Transferable_Title,
titleColor: textColor,
text: transferableText,
@@ -2933,7 +3061,7 @@ private final class GiftViewSheetContent: CombinedComponent {
items.append(
AnyComponentWithIdentity(
id: "tradable",
- component: AnyComponent(ParagraphComponent(
+ component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Upgrade_Tradable_Title,
titleColor: textColor,
text: tradableText,
@@ -3262,6 +3390,7 @@ private final class GiftViewSheetContent: CombinedComponent {
)
})
} else {
+ let descriptionConstrainedWidth = hasDescriptionButton ? context.availableSize.width - sideInset : context.availableSize.width - sideInset * 2.0 - 50.0
let description = description.update(
component: MultilineTextComponent(
text: .plain(attributedString),
@@ -3284,7 +3413,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
+ availableSize: CGSize(width: descriptionConstrainedWidth, height: CGFloat.greatestFiniteMagnitude),
transition: context.transition
)
descriptionSize = description.size
@@ -3311,7 +3440,7 @@ private final class GiftViewSheetContent: CombinedComponent {
animateScale: false
),
environment: {},
- availableSize: CGSize(width: description.size.width + 18.0, height: 19.0),
+ availableSize: CGSize(width: description.size.width + 18.0, height: description.size.height + 1.0),
transition: .immediate
)
headerComponents.append({
@@ -4072,7 +4201,7 @@ private final class GiftViewSheetContent: CombinedComponent {
HStack([
AnyComponentWithIdentity(
id: AnyHashable(0),
- component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))))
+ component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))))
),
AnyComponentWithIdentity(
id: AnyHashable(1),
@@ -4734,7 +4863,14 @@ private final class GiftViewSheetContent: CombinedComponent {
transition: .spring(duration: 0.2)
)
} else if upgraded, let arguments = subject.arguments, let upgradeMessageIdId = arguments.upgradeMessageId, let originalMessageId = arguments.messageId, !arguments.upgradeSeparate {
- let upgradeMessageId = MessageId(peerId: originalMessageId.peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId)
+ var delay = false
+ var peerId: EnginePeer.Id = originalMessageId.peerId
+ if peerId.isTelegramNotifications {
+ peerId = component.context.account.peerId
+ delay = true
+ }
+
+ let upgradeMessageId = MessageId(peerId: peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId)
let buttonTitle = strings.Gift_View_ViewUpgraded
buttonChild = button.update(
component: ButtonComponent(
@@ -4747,7 +4883,7 @@ private final class GiftViewSheetContent: CombinedComponent {
displaysProgress: false,
action: { [weak state] in
state?.dismiss(animated: true)
- state?.viewUpgradedGift(messageId: upgradeMessageId)
+ state?.viewUpgradedGift(messageId: upgradeMessageId, delay: delay)
}),
availableSize: buttonSize,
transition: context.transition
@@ -5165,7 +5301,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
case soldOutGift(StarGift.Gift)
case upgradePreview([StarGift.UniqueGift.Attribute], String)
- case wearPreview(StarGift.UniqueGift)
+ case wearPreview(StarGift, [StarGift.UniqueGift.Attribute]?)
var arguments: (
peerId: EnginePeer.Id?,
@@ -5197,13 +5333,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
prepaidUpgradeHash: String?,
upgradeSeparate: Bool,
dropOriginalDetailsStars: Int64?,
- auctionToPeerId: EnginePeer.Id?
+ auctionToPeerId: EnginePeer.Id?,
+ giftNumber: Int32?
)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
switch action.action {
- case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, _, toPeerId):
+ case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, _, toPeerId, number):
var reference: StarGiftReference
if let peerId, let giftMessageId {
reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId))
@@ -5214,8 +5351,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
let fromPeerId = senderId ?? message.author?.id
- return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId)
- case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _):
+ return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number)
+ case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@@ -5236,16 +5373,24 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
var resellAmounts: [CurrencyAmount]?
+ var number: Int32?
if case let .unique(uniqueGift) = gift {
resellAmounts = uniqueGift.resellAmounts
+ number = uniqueGift.number
+ }
+ return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil, number)
+ case let .starGiftPurchaseOffer(gift, _, _, _, _), let .starGiftPurchaseOfferDeclined(gift, _, _):
+ if case let .unique(gift) = gift {
+ return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil, nil)
+ } else {
+ return nil
}
- return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil)
default:
return nil
}
}
- case let .uniqueGift(gift, _), let .wearPreview(gift):
- return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil)
+ case let .uniqueGift(gift, _):
+ return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil, gift.number)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@@ -5255,11 +5400,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
resellAmounts = uniqueGift.resellAmounts
}
- return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil)
+ return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil, gift.number)
case .soldOutGift:
return nil
case .upgradePreview:
return nil
+ case let .wearPreview(gift, _):
+ return (nil, nil, nil, nil, nil, nil, false, gift, 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, nil, nil, nil, nil, nil, nil, false, nil, nil, nil)
}
return nil
}
@@ -5901,184 +6048,6 @@ private struct GiftConfiguration {
}
}
-private final class ParagraphComponent: CombinedComponent {
- let title: String
- let titleColor: UIColor
- let text: String
- let textColor: UIColor
- let accentColor: UIColor
- let iconName: String
- let iconColor: UIColor
- let badge: String?
- let action: () -> Void
-
- public init(
- title: String,
- titleColor: UIColor,
- text: String,
- textColor: UIColor,
- accentColor: UIColor,
- iconName: String,
- iconColor: UIColor,
- badge: String? = nil,
- action: @escaping () -> Void = {}
- ) {
- self.title = title
- self.titleColor = titleColor
- self.text = text
- self.textColor = textColor
- self.accentColor = accentColor
- self.iconName = iconName
- self.iconColor = iconColor
- self.badge = badge
- self.action = action
- }
-
- static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
- if lhs.title != rhs.title {
- return false
- }
- if lhs.titleColor != rhs.titleColor {
- return false
- }
- if lhs.text != rhs.text {
- return false
- }
- if lhs.textColor != rhs.textColor {
- return false
- }
- if lhs.accentColor != rhs.accentColor {
- return false
- }
- if lhs.iconName != rhs.iconName {
- return false
- }
- if lhs.iconColor != rhs.iconColor {
- return false
- }
- if lhs.badge != rhs.badge {
- return false
- }
- return true
- }
-
- static var body: Body {
- let title = Child(MultilineTextComponent.self)
- let text = Child(MultilineTextComponent.self)
- let icon = Child(BundleIconComponent.self)
- let badgeBackground = Child(RoundedRectangle.self)
- let badgeText = Child(MultilineTextComponent.self)
-
- return { context in
- let component = context.component
-
- let leftInset: CGFloat = 32.0
- let rightInset: CGFloat = 24.0
- let textSideInset: CGFloat = leftInset + 8.0
- let spacing: CGFloat = 5.0
-
- let textTopInset: CGFloat = 9.0
-
- let title = title.update(
- component: MultilineTextComponent(
- text: .plain(NSAttributedString(
- string: component.title,
- font: Font.semibold(15.0),
- textColor: component.titleColor,
- paragraphAlignment: .natural
- )),
- horizontalAlignment: .center,
- maximumNumberOfLines: 1
- ),
- availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
- transition: .immediate
- )
-
- let textFont = Font.regular(15.0)
- let boldTextFont = Font.semibold(15.0)
- let textColor = component.textColor
- let accentColor = component.accentColor
- let markdownAttributes = MarkdownAttributes(
- body: MarkdownAttributeSet(font: textFont, textColor: textColor),
- bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
- link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
- linkAttribute: { contents in
- return (TelegramTextAttributes.URL, contents)
- }
- )
-
- let text = text.update(
- component: MultilineTextComponent(
- text: .markdown(text: component.text, attributes: markdownAttributes),
- horizontalAlignment: .natural,
- maximumNumberOfLines: 0,
- lineSpacing: 0.2,
- highlightAction: { attributes in
- if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
- return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
- } else {
- return nil
- }
- },
- tapAction: { _, _ in
- component.action()
- }
- ),
- availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
- transition: .immediate
- )
-
- let icon = icon.update(
- component: BundleIconComponent(
- name: component.iconName,
- tintColor: component.iconColor
- ),
- availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
- transition: .immediate
- )
-
- context.add(title
- .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
- )
-
- if let badge = component.badge {
- let badgeText = badgeText.update(
- component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
- availableSize: context.availableSize,
- transition: context.transition
- )
-
- let badgeWidth = badgeText.size.width + 7.0
- let badgeBackground = badgeBackground.update(
- component: RoundedRectangle(
- color: component.accentColor,
- cornerRadius: 5.0),
- availableSize: CGSize(width: badgeWidth, height: 16.0),
- transition: context.transition
- )
-
- context.add(badgeBackground
- .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
- )
-
- context.add(badgeText
- .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
- )
- }
-
- context.add(text
- .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
- )
-
- context.add(icon
- .position(CGPoint(x: 15.0, y: textTopInset + 18.0))
- )
-
- return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
- }
- }
-}
-
private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource {
private let controller: ViewController
private let sourceNode: ASDisplayNode
@@ -6196,7 +6165,7 @@ private final class HeaderButtonComponent: CombinedComponent {
}
}
-private final class AvatarComponent: Component {
+final class AvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift
index eb9e874dd7..8d128e4a0a 100644
--- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift
+++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift
@@ -53,10 +53,12 @@ final class TableComponent: CombinedComponent {
private let theme: PresentationTheme
private let items: [Item]
-
- public init(theme: PresentationTheme, items: [Item]) {
+ private let semiTransparent: Bool
+
+ public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) {
self.theme = theme
self.items = items
+ self.semiTransparent = semiTransparent
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
@@ -66,6 +68,9 @@ final class TableComponent: CombinedComponent {
if lhs.items != rhs.items {
return false
}
+ if lhs.semiTransparent != rhs.semiTransparent {
+ return false
+ }
return true
}
@@ -95,7 +100,10 @@ final class TableComponent: CombinedComponent {
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
- let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
+ var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor
+ if context.component.semiTransparent {
+ secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5)
+ }
var leftColumnWidth: CGFloat = 0.0
diff --git a/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD
new file mode 100644
index 0000000000..0f3e51fdcd
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD
@@ -0,0 +1,28 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "InfoParagraphComponent",
+ module_name = "InfoParagraphComponent",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AsyncDisplayKit",
+ "//submodules/Display",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ComponentDisplayAdapters",
+ "//submodules/TelegramPresentationData",
+ "//submodules/AccountContext",
+ "//submodules/AppBundle",
+ "//submodules/Components/MultilineTextComponent",
+ "//submodules/Components/BundleIconComponent",
+ "//submodules/Markdown",
+ "//submodules/TextFormat",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift
new file mode 100644
index 0000000000..7e3650cec4
--- /dev/null
+++ b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift
@@ -0,0 +1,187 @@
+import Foundation
+import UIKit
+import Display
+import TelegramPresentationData
+import ComponentFlow
+import MultilineTextComponent
+import BundleIconComponent
+import Markdown
+import TextFormat
+
+public final class InfoParagraphComponent: CombinedComponent {
+ let title: String
+ let titleColor: UIColor
+ let text: String
+ let textColor: UIColor
+ let accentColor: UIColor
+ let iconName: String
+ let iconColor: UIColor
+ let badge: String?
+ let action: () -> Void
+
+ public init(
+ title: String,
+ titleColor: UIColor,
+ text: String,
+ textColor: UIColor,
+ accentColor: UIColor,
+ iconName: String,
+ iconColor: UIColor,
+ badge: String? = nil,
+ action: @escaping () -> Void = {}
+ ) {
+ self.title = title
+ self.titleColor = titleColor
+ self.text = text
+ self.textColor = textColor
+ self.accentColor = accentColor
+ self.iconName = iconName
+ self.iconColor = iconColor
+ self.badge = badge
+ self.action = action
+ }
+
+ public static func ==(lhs: InfoParagraphComponent, rhs: InfoParagraphComponent) -> Bool {
+ if lhs.title != rhs.title {
+ return false
+ }
+ if lhs.titleColor != rhs.titleColor {
+ return false
+ }
+ if lhs.text != rhs.text {
+ return false
+ }
+ if lhs.textColor != rhs.textColor {
+ return false
+ }
+ if lhs.accentColor != rhs.accentColor {
+ return false
+ }
+ if lhs.iconName != rhs.iconName {
+ return false
+ }
+ if lhs.iconColor != rhs.iconColor {
+ return false
+ }
+ if lhs.badge != rhs.badge {
+ return false
+ }
+ return true
+ }
+
+ public static var body: Body {
+ let title = Child(MultilineTextComponent.self)
+ let text = Child(MultilineTextComponent.self)
+ let icon = Child(BundleIconComponent.self)
+ let badgeBackground = Child(RoundedRectangle.self)
+ let badgeText = Child(MultilineTextComponent.self)
+
+ return { context in
+ let component = context.component
+
+ let leftInset: CGFloat = 32.0
+ let rightInset: CGFloat = 24.0
+ let textSideInset: CGFloat = leftInset + 8.0
+ let spacing: CGFloat = 5.0
+
+ let textTopInset: CGFloat = 9.0
+
+ let title = title.update(
+ component: MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: component.title,
+ font: Font.semibold(15.0),
+ textColor: component.titleColor,
+ paragraphAlignment: .natural
+ )),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 1
+ ),
+ availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
+ transition: .immediate
+ )
+
+ let textFont = Font.regular(15.0)
+ let boldTextFont = Font.semibold(15.0)
+ let textColor = component.textColor
+ let accentColor = component.accentColor
+ let markdownAttributes = MarkdownAttributes(
+ body: MarkdownAttributeSet(font: textFont, textColor: textColor),
+ bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
+ link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
+ linkAttribute: { contents in
+ return (TelegramTextAttributes.URL, contents)
+ }
+ )
+
+ let text = text.update(
+ component: MultilineTextComponent(
+ text: .markdown(text: component.text, attributes: markdownAttributes),
+ horizontalAlignment: .natural,
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.2,
+ highlightAction: { attributes in
+ if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
+ return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
+ } else {
+ return nil
+ }
+ },
+ tapAction: { _, _ in
+ component.action()
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
+ transition: .immediate
+ )
+
+ let icon = icon.update(
+ component: BundleIconComponent(
+ name: component.iconName,
+ tintColor: component.iconColor
+ ),
+ availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
+ transition: .immediate
+ )
+
+ context.add(title
+ .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
+ )
+
+ if let badge = component.badge {
+ let badgeText = badgeText.update(
+ component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
+ availableSize: context.availableSize,
+ transition: context.transition
+ )
+
+ let badgeWidth = badgeText.size.width + 7.0
+ let badgeBackground = badgeBackground.update(
+ component: RoundedRectangle(
+ color: component.accentColor,
+ cornerRadius: 5.0),
+ availableSize: CGSize(width: badgeWidth, height: 16.0),
+ transition: context.transition
+ )
+
+ context.add(badgeBackground
+ .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
+ )
+
+ context.add(badgeText
+ .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
+ )
+ }
+
+ context.add(text
+ .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
+ )
+
+ context.add(icon
+ .position(CGPoint(x: 15.0, y: textTopInset + 18.0))
+ )
+
+ return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift
index 95d8c40e2e..4c1852d959 100644
--- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift
+++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift
@@ -501,7 +501,7 @@ public class GlassBackgroundView: UIView {
let glassEffect = UIGlassEffect(style: .regular)
switch tintColor.kind {
case .panel:
- glassEffect.tintColor = nil
+ glassEffect.tintColor = UIColor(white: isDark ? 0.0 : 1.0, alpha: 0.1)
case .custom:
glassEffect.tintColor = tintColor.color
}
@@ -519,8 +519,8 @@ public class GlassBackgroundView: UIView {
nativeParamsView.lumaMin = 0.0
nativeParamsView.lumaMax = 0.15
} else {
- nativeParamsView.lumaMin = 0.25
- nativeParamsView.lumaMax = 1.0
+ nativeParamsView.lumaMin = 0.6
+ nativeParamsView.lumaMax = 0.61
}
}
}
@@ -609,8 +609,8 @@ public final class GlassBackgroundContainerView: UIView {
nativeParamsView.lumaMin = 0.0
nativeParamsView.lumaMax = 0.15
} else {
- nativeParamsView.lumaMin = 0.25
- nativeParamsView.lumaMax = 1.0
+ nativeParamsView.lumaMin = 0.6
+ nativeParamsView.lumaMax = 0.61
}
transition.setFrame(view: nativeView, frame: CGRect(origin: CGPoint(), size: size))
diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift
index bee29b5e40..6b012d1097 100644
--- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift
+++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift
@@ -79,6 +79,7 @@ public final class GlassBarButtonComponent: Component {
super.init(frame: frame)
self.containerView.isUserInteractionEnabled = false
+ self.containerView.layer.rasterizationScale = UIScreenScale
self.addSubview(self.containerView)
self.containerView.addSubview(self.genericContainerView)
diff --git a/submodules/TelegramUI/Components/LiquidLens/BUILD b/submodules/TelegramUI/Components/LiquidLens/BUILD
new file mode 100644
index 0000000000..7b57943ddc
--- /dev/null
+++ b/submodules/TelegramUI/Components/LiquidLens/BUILD
@@ -0,0 +1,20 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "LiquidLens",
+ module_name = "LiquidLens",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/Display",
+ "//submodules/ComponentFlow",
+ "//submodules/TelegramUI/Components/GlassBackgroundComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift
new file mode 100644
index 0000000000..e5d1e84948
--- /dev/null
+++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift
@@ -0,0 +1,373 @@
+import Foundation
+import UIKit
+import Display
+import ComponentFlow
+import GlassBackgroundComponent
+
+private final class RestingBackgroundView: UIVisualEffectView {
+ var isDark: Bool?
+
+ static func colorMatrix(isDark: Bool) -> [Float32] {
+ if isDark {
+ return [1.082, -0.113, -0.011, 0.0, 0.135, -0.034, 1.003, -0.011, 0.0, 0.135, -0.034, -0.113, 1.105, 0.0, 0.135, 0.0, 0.0, 0.0, 1.0, 0.0]
+ } else {
+ return [1.185, -0.05, -0.005, 0.0, -0.2, -0.015, 1.15, -0.005, 0.0, -0.2, -0.015, -0.05, 1.195, 0.0, -0.2, 0.0, 0.0, 0.0, 1.0, 0.0]
+ }
+ }
+
+ init() {
+ let effect = UIBlurEffect(style: .light)
+ super.init(effect: effect)
+
+ for subview in self.subviews {
+ if subview.description.contains("VisualEffectSubview") {
+ subview.isHidden = true
+ }
+ }
+
+ self.clipsToBounds = true
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(isDark: Bool) {
+ if self.isDark == isDark {
+ return
+ }
+ self.isDark = isDark
+
+ if let sublayer = self.layer.sublayers?[0], let _ = sublayer.filters {
+ sublayer.backgroundColor = nil
+ sublayer.isOpaque = false
+
+ if let classValue = NSClassFromString("CAFilter") as AnyObject as? NSObjectProtocol {
+ let makeSelector = NSSelectorFromString("filterWithName:")
+ let filter = classValue.perform(makeSelector, with: "colorMatrix").takeUnretainedValue() as? NSObject
+
+ if let filter {
+ var matrix: [Float32] = RestingBackgroundView.colorMatrix(isDark: isDark)
+ filter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix")
+ sublayer.filters = [filter]
+ sublayer.setValue(1.0, forKey: "scale")
+ }
+ }
+ }
+ }
+}
+
+public final class LiquidLensView: UIView {
+ private struct Params: Equatable {
+ var size: CGSize
+ var selectionX: CGFloat
+ var selectionWidth: CGFloat
+ var isDark: Bool
+ var isLifted: Bool
+
+ init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool) {
+ self.size = size
+ self.selectionX = selectionX
+ self.selectionWidth = selectionWidth
+ self.isLifted = isLifted
+ self.isDark = isDark
+ }
+ }
+
+ private struct LensParams: Equatable {
+ var baseFrame: CGRect
+ var isLifted: Bool
+
+ init(baseFrame: CGRect, isLifted: Bool) {
+ self.baseFrame = baseFrame
+ self.isLifted = isLifted
+ }
+ }
+
+ private let containerView: UIView
+ private let backgroundContainerContainer: UIView
+ private let backgroundContainer: GlassBackgroundContainerView
+ private let backgroundView: GlassBackgroundView
+ private var lensView: UIView?
+ private let liftedContainerView: UIView
+ public let contentView: UIView
+ private let restingBackgroundView: RestingBackgroundView
+
+ private var legacySelectionView: GlassBackgroundView.ContentImageView?
+ private var legacyContentMaskView: UIView?
+ private var legacyContentMaskBlobView: UIImageView?
+ private var legacyLiftedContentBlobMaskView: UIImageView?
+
+ public var selectedContentView: UIView {
+ return self.liftedContainerView
+ }
+
+ private var params: Params?
+ private var appliedLensParams: LensParams?
+ private var isApplyingLensParams: Bool = false
+ private var pendingLensParams: LensParams?
+
+ private var liftedDisplayLink: SharedDisplayLinkDriver.Link?
+
+ public var selectionX: CGFloat? {
+ return self.params?.selectionX
+ }
+
+ public var selectionWidth: CGFloat? {
+ return self.params?.selectionWidth
+ }
+
+ override public init(frame: CGRect) {
+ self.containerView = UIView()
+
+ self.backgroundContainerContainer = UIView()
+ self.backgroundContainer = GlassBackgroundContainerView()
+
+ self.backgroundView = GlassBackgroundView()
+
+ self.contentView = UIView()
+ self.liftedContainerView = UIView()
+
+ self.restingBackgroundView = RestingBackgroundView()
+
+ super.init(frame: frame)
+
+ self.backgroundContainerContainer.addSubview(self.backgroundContainer)
+ self.addSubview(self.backgroundContainerContainer)
+
+ self.backgroundContainer.contentView.addSubview(self.backgroundView)
+ self.backgroundView.contentView.addSubview(self.containerView)
+ self.containerView.isUserInteractionEnabled = false
+
+ if #available(iOS 26.0, *) {
+ if let viewClass = NSClassFromString("_UILiquidLensView") as AnyObject as? NSObjectProtocol {
+ let allocSelector = NSSelectorFromString("alloc")
+ let initSelector = NSSelectorFromString("initWithRestingBackground:")
+ let objcAlloc = viewClass.perform(allocSelector).takeUnretainedValue()
+ let instance = objcAlloc.perform(initSelector, with: UIView()).takeUnretainedValue()
+ self.lensView = instance as? UIView
+ }
+ }
+
+ if let lensView = self.lensView {
+ self.backgroundContainer.layer.zPosition = 1
+ lensView.layer.zPosition = 10.0
+
+ self.liftedContainerView.addSubview(self.restingBackgroundView)
+
+ self.containerView.addSubview(self.liftedContainerView)
+ self.containerView.addSubview(lensView)
+ self.containerView.addSubview(self.contentView)
+
+ lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: self.backgroundContainer.contentView)
+ lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView)
+ lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView)
+
+ do {
+ let selector = NSSelectorFromString("setLiftedContentMode:")
+ if let method = lensView.method(for: selector) {
+ typealias ObjCMethod = @convention(c) (AnyObject, Selector, Int32) -> Void
+ let function = unsafeBitCast(method, to: ObjCMethod.self)
+ function(lensView, selector, 1)
+ }
+ }
+
+ do {
+ let selector = NSSelectorFromString("setStyle:")
+ if let method = lensView.method(for: selector) {
+ typealias ObjCMethod = @convention(c) (AnyObject, Selector, Int32) -> Void
+ let function = unsafeBitCast(method, to: ObjCMethod.self)
+ function(lensView, selector, 1)
+ }
+ }
+
+ do {
+ let selector = NSSelectorFromString("setWarpsContentBelow:")
+ if let method = lensView.method(for: selector) {
+ typealias ObjCMethod = @convention(c) (AnyObject, Selector, Bool) -> Void
+ let function = unsafeBitCast(method, to: ObjCMethod.self)
+ function(lensView, selector, true)
+ }
+ }
+
+ lensView.setValue(UIColor(white: 0.0, alpha: 0.1), forKey: "restingBackgroundColor")
+ } else {
+ let legacySelectionView = GlassBackgroundView.ContentImageView()
+ self.legacySelectionView = legacySelectionView
+ self.backgroundView.contentView.insertSubview(legacySelectionView, at: 0)
+
+ let legacyContentMaskView = UIView()
+ legacyContentMaskView.backgroundColor = .white
+ self.legacyContentMaskView = legacyContentMaskView
+ self.contentView.mask = legacyContentMaskView
+
+ if let filter = CALayer.luminanceToAlpha() {
+ legacyContentMaskView.layer.filters = [filter]
+ }
+
+ let legacyContentMaskBlobView = UIImageView()
+ self.legacyContentMaskBlobView = legacyContentMaskBlobView
+ legacyContentMaskView.addSubview(legacyContentMaskBlobView)
+
+ self.containerView.addSubview(self.contentView)
+
+ let legacyLiftedContentBlobMaskView = UIImageView()
+ self.legacyLiftedContentBlobMaskView = legacyLiftedContentBlobMaskView
+ self.liftedContainerView.mask = legacyLiftedContentBlobMaskView
+
+ self.containerView.addSubview(self.liftedContainerView)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, transition: ComponentTransition) {
+ let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted)
+ if self.params == params {
+ return
+ }
+ self.update(params: params, transition: transition)
+ }
+
+ private func update(transition: ComponentTransition) {
+ guard let params = self.params else {
+ return
+ }
+ self.update(params: params, transition: transition)
+ }
+
+ private func updateLens(params: LensParams, animated: Bool) {
+ guard let lensView = self.lensView else {
+ return
+ }
+
+ if self.isApplyingLensParams {
+ self.pendingLensParams = params
+ return
+ }
+ self.isApplyingLensParams = true
+ let previousParams = self.appliedLensParams
+
+ let transition: ComponentTransition = animated ? .easeInOut(duration: 0.3) : .immediate
+
+ if previousParams?.isLifted != params.isLifted {
+ let selector = NSSelectorFromString("setLifted:animated:alongsideAnimations:completion:")
+ var shouldScheduleUpdate = false
+ var didProcessUpdate = false
+ self.pendingLensParams = params
+ if let lensView = self.lensView, let method = lensView.method(for: selector) {
+ typealias ObjCMethod = @convention(c) (AnyObject, Selector, Bool, Bool, @escaping () -> Void, AnyObject?) -> Void
+ let function = unsafeBitCast(method, to: ObjCMethod.self)
+ function(lensView, selector, params.isLifted, !transition.animation.isImmediate, { [weak self] in
+ guard let self else {
+ return
+ }
+ let liftedInset: CGFloat = params.isLifted ? 4.0 : -4.0
+ lensView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: params.baseFrame.width + liftedInset * 2.0, height: params.baseFrame.height + liftedInset * 2.0))
+ didProcessUpdate = true
+ if shouldScheduleUpdate {
+ DispatchQueue.main.async { [weak self] in
+ guard let self, let pendingLensParams = self.pendingLensParams else {
+ return
+ }
+ self.isApplyingLensParams = false
+ self.pendingLensParams = nil
+ self.updateLens(params: pendingLensParams, animated: !transition.animation.isImmediate)
+ }
+ }
+ }, nil)
+ }
+ if didProcessUpdate {
+ transition.animateView {
+ lensView.center = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY)
+ }
+ self.pendingLensParams = nil
+ self.isApplyingLensParams = false
+ } else {
+ shouldScheduleUpdate = true
+ }
+ } else {
+ transition.animateView {
+ let liftedInset: CGFloat = params.isLifted ? 4.0 : -4.0
+ lensView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: params.baseFrame.width + liftedInset * 2.0, height: params.baseFrame.height + liftedInset * 2.0))
+ lensView.center = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY)
+ }
+ self.isApplyingLensParams = false
+ }
+ }
+
+ private func updateLiftedLensPosition() {
+ // Without this, the lens won't update its bouncing animations unless it's being moved
+ if self.isApplyingLensParams {
+ return
+ }
+ guard let lensView = self.lensView else {
+ return
+ }
+ guard let params = self.appliedLensParams else {
+ return
+ }
+ lensView.center = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY)
+ }
+
+ private func update(params: Params, transition: ComponentTransition) {
+ let isFirstTime = self.params == nil
+ let transition: ComponentTransition = isFirstTime ? .immediate : transition
+
+ self.params = params
+
+ transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: params.size))
+ transition.setFrame(view: self.backgroundContainerContainer, frame: CGRect(origin: CGPoint(), size: params.size))
+
+ transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size))
+ self.backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition)
+
+ transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
+ self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
+
+ transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size))
+ transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size))
+
+ let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height))
+ self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate)
+
+ if let legacyContentMaskView = self.legacyContentMaskView {
+ transition.setFrame(view: legacyContentMaskView, frame: CGRect(origin: CGPoint(), size: params.size))
+ }
+ if let legacyContentMaskBlobView = self.legacyContentMaskBlobView, let legacyLiftedContentBlobMaskView = self.legacyLiftedContentBlobMaskView, let legacySelectionView = self.legacySelectionView {
+ let lensFrame = baseLensFrame.insetBy(dx: 4.0, dy: 4.0)
+ let effectiveLensFrame = lensFrame.insetBy(dx: params.isLifted ? -2.0 : 0.0, dy: params.isLifted ? -2.0 : 0.0)
+
+ if legacyContentMaskBlobView.image?.size.height != lensFrame.height {
+ legacyContentMaskBlobView.image = generateStretchableFilledCircleImage(diameter: lensFrame.height, color: .black)
+ legacyLiftedContentBlobMaskView.image = legacyContentMaskBlobView.image
+ legacySelectionView.image = generateStretchableFilledCircleImage(diameter: lensFrame.height, color: .white)?.withRenderingMode(.alwaysTemplate)
+ }
+ transition.setFrame(view: legacyContentMaskBlobView, frame: effectiveLensFrame)
+ transition.setFrame(view: legacyLiftedContentBlobMaskView, frame: effectiveLensFrame)
+
+ legacySelectionView.tintColor = UIColor(white: params.isDark ? 1.0 : 0.0, alpha: params.isDark ? 0.1 : 0.075)
+ transition.setFrame(view: legacySelectionView, frame: effectiveLensFrame)
+ }
+
+ transition.setFrame(view: self.restingBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size))
+ self.restingBackgroundView.update(isDark: params.isDark)
+ transition.setAlpha(view: self.restingBackgroundView, alpha: params.isLifted ? 0.0 : 1.0)
+
+ if params.isLifted {
+ if self.liftedDisplayLink == nil {
+ self.liftedDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
+ guard let self else {
+ return
+ }
+ self.updateLiftedLensPosition()
+ })
+ }
+ } else if let liftedDisplayLink = self.liftedDisplayLink {
+ self.liftedDisplayLink = nil
+ liftedDisplayLink.invalidate()
+ }
+ }
+}
diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD
index e9b7043eb5..c656c7429c 100644
--- a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD
+++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD
@@ -16,6 +16,7 @@ swift_library(
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/SwitchNode",
"//submodules/CheckNode",
+ "//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift
new file mode 100644
index 0000000000..2fd83b0272
--- /dev/null
+++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift
@@ -0,0 +1,276 @@
+import Foundation
+import UIKit
+import Display
+import ComponentFlow
+import MultilineTextComponent
+
+private final class ContextOptionComponent: Component {
+ let title: String
+ let color: UIColor
+ let isLast: Bool
+ let action: () -> Void
+
+ init(
+ title: String,
+ color: UIColor,
+ isLast: Bool,
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.color = color
+ self.isLast = isLast
+ self.action = action
+ }
+
+ static func ==(lhs: ContextOptionComponent, rhs: ContextOptionComponent) -> Bool {
+ if lhs.title != rhs.title {
+ return false
+ }
+ if lhs.color != rhs.color {
+ return false
+ }
+ if lhs.isLast != rhs.isLast {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ let backgroundView: UIView
+ let title = ComponentView()
+
+ var component: ContextOptionComponent?
+
+ override init(frame: CGRect) {
+ self.backgroundView = UIView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.backgroundView)
+ self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
+ if case .ended = recognizer.state {
+ self.component?.action()
+ }
+ }
+
+ func update(component: ContextOptionComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+
+ let sideInset: CGFloat = 8.0
+
+ let titleSize = self.title.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: .white))
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
+ )
+ let size = CGSize(width: sideInset * 2.0 + titleSize.width, height: availableSize.height)
+ let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((size.height - titleSize.height) * 0.5)), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.addSubview(titleView)
+ }
+ transition.setPosition(view: titleView, position: titleFrame.center)
+ titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
+ }
+
+ self.backgroundView.backgroundColor = component.color
+ transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width + (component.isLast ? 1000.0 : 0.0), height: size.height)))
+
+ return size
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+final class ContentContainer: UIScrollView, UIScrollViewDelegate {
+ private let closeOtherContextOptions: () -> Void
+
+ private var itemViews: [AnyHashable: ComponentView] = [:]
+
+ private var ignoreScrollingEvents: Bool = false
+ private var draggingBeganInClosedState: Bool = false
+ private var didProcessScrollingCycle: Bool = false
+
+ private var contextOptions: [ListActionItemComponent.ContextOption] = []
+ private var optionsWidth: CGFloat = 0.0
+
+ private var revealedStateTapRecognizer: UITapGestureRecognizer?
+
+ init(closeOtherContextOptions: @escaping () -> Void) {
+ self.closeOtherContextOptions = closeOtherContextOptions
+
+ super.init(frame: CGRect())
+
+ let revealedStateTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))
+ self.revealedStateTapRecognizer = revealedStateTapRecognizer
+ revealedStateTapRecognizer.isEnabled = false
+ self.addGestureRecognizer(revealedStateTapRecognizer)
+
+ self.delaysContentTouches = false
+ self.canCancelContentTouches = true
+ self.clipsToBounds = false
+ self.contentInsetAdjustmentBehavior = .never
+ self.automaticallyAdjustsScrollIndicatorInsets = false
+ self.showsVerticalScrollIndicator = false
+ self.showsHorizontalScrollIndicator = false
+ self.alwaysBounceHorizontal = false
+ self.alwaysBounceVertical = false
+ self.scrollsToTop = false
+ self.delegate = self
+
+ self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
+ guard let self else {
+ return false
+ }
+
+ if self.contentOffset.x != 0.0 {
+ return true
+ }
+
+ return false
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func onTapGesture(_ recognizer: UITapGestureRecognizer) {
+ if case .ended = recognizer.state {
+ self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
+ }
+ }
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ self.revealedStateTapRecognizer?.isEnabled = self.contentOffset.x > 0.0
+ if self.contentOffset.x > 0.0 {
+ if !self.didProcessScrollingCycle {
+ self.didProcessScrollingCycle = true
+ self.closeOtherContextOptions()
+ }
+ }
+ }
+
+ func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
+ self.didProcessScrollingCycle = false
+ self.draggingBeganInClosedState = self.contentOffset.x == 0.0
+ }
+
+ func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
+ targetContentOffset.pointee.x = self.contentOffset.x
+
+ if self.contentOffset.x >= self.optionsWidth + 30.0 {
+ self.contextOptions.last?.action()
+ } else {
+ DispatchQueue.main.async { [weak self] in
+ guard let self else {
+ return
+ }
+ if self.draggingBeganInClosedState {
+ if self.contentOffset.x > 20.0 {
+ self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true)
+ } else {
+ self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
+ }
+ } else {
+ if self.contentOffset.x < self.optionsWidth - 20.0 {
+ self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
+ } else {
+ self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true)
+ }
+ }
+ }
+ }
+ }
+
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ if let revealedStateTapRecognizer = self.revealedStateTapRecognizer, revealedStateTapRecognizer.isEnabled {
+ if self.bounds.contains(point), point.x < self.bounds.width {
+ return self
+ }
+ }
+ guard let result = super.hitTest(point, with: event) else {
+ return nil
+ }
+ return result
+ }
+
+ func closeContextOptions() {
+ self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
+ }
+
+ func update(size: CGSize, contextOptions: [ListActionItemComponent.ContextOption], transition: ComponentTransition) {
+ self.contextOptions = contextOptions
+
+ var validIds: [AnyHashable] = []
+ var optionsWidth: CGFloat = 0.0
+ for i in 0 ..< contextOptions.count {
+ let option = contextOptions[i]
+ validIds.append(option.id)
+
+ let itemView: ComponentView
+ var itemTransition = transition
+ if let current = self.itemViews[option.id] {
+ itemView = current
+ } else {
+ itemTransition = itemTransition.withAnimation(.none)
+ itemView = ComponentView()
+ self.itemViews[option.id] = itemView
+ }
+
+ let itemSize = itemView.update(
+ transition: itemTransition,
+ component: AnyComponent(ContextOptionComponent(
+ title: option.title,
+ color: option.color,
+ isLast: i == contextOptions.count - 1,
+ action: option.action
+ )),
+ environment: {},
+ containerSize: CGSize(width: 10000.0, height: size.height)
+ )
+ let itemFrame = CGRect(origin: CGPoint(x: size.width + optionsWidth, y: 0.0), size: itemSize)
+ optionsWidth += itemSize.width
+ if let itemComponentView = itemView.view {
+ self.addSubview(itemComponentView)
+ itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
+ }
+ }
+ var removedIds: [AnyHashable] = []
+ for (id, itemView) in self.itemViews {
+ if !validIds.contains(id) {
+ removedIds.append(id)
+ if let itemComponentView = itemView.view {
+ itemComponentView.removeFromSuperview()
+ }
+ }
+ }
+ for id in removedIds {
+ self.itemViews.removeValue(forKey: id)
+ }
+ self.optionsWidth = optionsWidth
+
+ let contentSize = CGSize(width: size.width + optionsWidth, height: size.height)
+ if self.contentSize != contentSize {
+ self.contentSize = contentSize
+ }
+ self.isScrollEnabled = optionsWidth != 0.0
+ }
+}
diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift
index d9abc93708..435802b919 100644
--- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift
+++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift
@@ -70,6 +70,7 @@ public final class ListActionItemComponent: Component {
case arrow
case toggle(Toggle)
case activity
+ case expandArrows
case custom(CustomAccessory)
}
@@ -137,6 +138,33 @@ public final class ListActionItemComponent: Component {
case center
}
+ public final class ContextOption: Equatable {
+ public let id: AnyHashable
+ public let title: String
+ public let color: UIColor
+ public let action: () -> Void
+
+ public init(id: AnyHashable, title: String, color: UIColor, action: @escaping () -> Void) {
+ self.id = id
+ self.title = title
+ self.color = color
+ self.action = action
+ }
+
+ public static func ==(lhs: ContextOption, rhs: ContextOption) -> Bool {
+ if lhs.id != rhs.id {
+ return false
+ }
+ if lhs.title != rhs.title {
+ return false
+ }
+ if lhs.color != rhs.color {
+ return false
+ }
+ return true
+ }
+ }
+
public let theme: PresentationTheme
public let style: Style
public let background: AnyComponent?
@@ -146,6 +174,7 @@ public final class ListActionItemComponent: Component {
public let leftIcon: LeftIcon?
public let icon: Icon?
public let accessory: Accessory?
+ public let contextOptions: [ContextOption]
public let action: ((UIView) -> Void)?
public let highlighting: Highlighting
public let updateIsHighlighted: ((UIView, Bool) -> Void)?
@@ -160,6 +189,7 @@ public final class ListActionItemComponent: Component {
leftIcon: LeftIcon? = nil,
icon: Icon? = nil,
accessory: Accessory? = .arrow,
+ contextOptions: [ContextOption] = [],
action: ((UIView) -> Void)?,
highlighting: Highlighting = .default,
updateIsHighlighted: ((UIView, Bool) -> Void)? = nil
@@ -173,6 +203,7 @@ public final class ListActionItemComponent: Component {
self.leftIcon = leftIcon
self.icon = icon
self.accessory = accessory
+ self.contextOptions = contextOptions
self.action = action
self.highlighting = highlighting
self.updateIsHighlighted = updateIsHighlighted
@@ -206,6 +237,9 @@ public final class ListActionItemComponent: Component {
if lhs.accessory != rhs.accessory {
return false
}
+ if lhs.contextOptions != rhs.contextOptions {
+ return false
+ }
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
@@ -288,7 +322,9 @@ public final class ListActionItemComponent: Component {
}
}
- public final class View: HighlightTrackingButton, ListSectionComponent.ChildView {
+ public final class View: UIView, ListSectionComponent.ChildView {
+ private let container: ContentContainer
+ private let button: HighlightTrackingButton
private var background: ComponentView?
private let title = ComponentView()
private var leftIcon: ComponentView?
@@ -312,13 +348,23 @@ public final class ListActionItemComponent: Component {
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
+ var closeOtherContextOptions: (() -> Void)?
+ self.container = ContentContainer(closeOtherContextOptions: {
+ closeOtherContextOptions?()
+ })
+ self.button = HighlightTrackingButton()
+
super.init(frame: CGRect())
- self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
- self.internalHighligthedChanged = { [weak self] isHighlighted in
+ self.addSubview(self.container)
+ self.container.addSubview(self.button)
+
+ self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
+ self.button.internalHighligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, component.action != nil else {
return
}
@@ -333,6 +379,20 @@ public final class ListActionItemComponent: Component {
customUpdateIsHighlighted(isHighlighted)
}
}
+
+ closeOtherContextOptions = { [weak self] in
+ guard let self else {
+ return
+ }
+ self.enumerateSiblings?({ sibling in
+ if self === sibling {
+ return
+ }
+ if let view = sibling as? View {
+ view.container.closeContextOptions()
+ }
+ })
+ }
}
required public init?(coder: NSCoder) {
@@ -373,6 +433,8 @@ public final class ListActionItemComponent: Component {
}
case .arrow:
contentRightInset = 30.0
+ case .expandArrows:
+ contentRightInset = 36.0
case .toggle:
contentRightInset = 76.0
case .activity:
@@ -476,7 +538,7 @@ public final class ListActionItemComponent: Component {
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
if let iconView = icon.view {
if iconView.superview == nil {
- self.addSubview(iconView)
+ self.button.addSubview(iconView)
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
}
iconView.isUserInteractionEnabled = iconValue.allowUserInteraction
@@ -513,7 +575,7 @@ public final class ListActionItemComponent: Component {
animateIn = true
leftCheckView = CheckView()
self.leftCheckView = leftCheckView
- self.addSubview(leftCheckView)
+ self.button.addSubview(leftCheckView)
leftCheckView.action = { [weak self] in
guard let self, let component = self.component else {
@@ -593,7 +655,7 @@ public final class ListActionItemComponent: Component {
if let leftIconView = leftIcon.view {
if leftIconView.superview == nil {
leftIconView.isUserInteractionEnabled = false
- self.addSubview(leftIconView)
+ self.button.addSubview(leftIconView)
transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0)
}
leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame)
@@ -631,7 +693,7 @@ public final class ListActionItemComponent: Component {
arrowTransition = arrowTransition.withAnimation(.none)
arrowView = UIImageView(image: PresentationResourcesItemList.disclosureArrowImage(component.theme))
self.arrowView = arrowView
- self.addSubview(arrowView)
+ self.button.addSubview(arrowView)
}
if let image = arrowView.image {
@@ -645,6 +707,32 @@ public final class ListActionItemComponent: Component {
}
}
+ if case .expandArrows = component.accessory {
+ let arrowView: UIImageView
+ var arrowTransition = transition
+ if let current = self.arrowView {
+ arrowView = current
+ if themeUpdated {
+ arrowView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme)
+ }
+ } else {
+ arrowTransition = arrowTransition.withAnimation(.none)
+ arrowView = UIImageView(image: PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme))
+ self.arrowView = arrowView
+ self.button.addSubview(arrowView)
+ }
+
+ if let image = arrowView.image {
+ let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size)
+ arrowTransition.setFrame(view: arrowView, frame: arrowFrame)
+ }
+ } else {
+ if let arrowView = self.arrowView {
+ self.arrowView = nil
+ arrowView.removeFromSuperview()
+ }
+ }
+
if case let .toggle(toggle) = component.accessory {
switch toggle.style {
case .regular:
@@ -660,7 +748,7 @@ public final class ListActionItemComponent: Component {
switchNode = SwitchNode()
switchNode.setOn(toggle.isOn, animated: false)
self.switchNode = switchNode
- self.addSubview(switchNode.view)
+ self.button.addSubview(switchNode.view)
switchNode.valueUpdated = { [weak self] value in
guard let self, let component = self.component else {
@@ -710,7 +798,7 @@ public final class ListActionItemComponent: Component {
switchNode.updateIsLocked(toggle.style == .lock)
switchNode.setOn(toggle.isOn, animated: false)
self.iconSwitchNode = switchNode
- self.addSubview(switchNode.view)
+ self.button.addSubview(switchNode.view)
switchNode.valueUpdated = { [weak self] value in
guard let self, let component = self.component else {
@@ -763,7 +851,7 @@ public final class ListActionItemComponent: Component {
activityIndicatorView = UIActivityIndicatorView(style: .gray)
}
self.activityIndicatorView = activityIndicatorView
- self.addSubview(activityIndicatorView)
+ self.button.addSubview(activityIndicatorView)
activityIndicatorView.sizeToFit()
}
@@ -789,7 +877,7 @@ public final class ListActionItemComponent: Component {
if let customAccessoryComponentView = customAccessoryView.view {
if customAccessoryComponentView.superview == nil {
customAccessoryComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
- self.addSubview(customAccessoryComponentView)
+ self.button.addSubview(customAccessoryComponentView)
}
customAccessoryComponentView.isUserInteractionEnabled = customAccessory.isInteractive
customAccessoryTransition.setPosition(view: customAccessoryComponentView, position: CGPoint(x: activityAccessoryFrame.maxX, y: activityAccessoryFrame.minY))
@@ -806,7 +894,7 @@ public final class ListActionItemComponent: Component {
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
- self.addSubview(titleView)
+ self.button.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
@@ -850,7 +938,12 @@ public final class ListActionItemComponent: Component {
}
}
- return CGSize(width: availableSize.width, height: contentHeight)
+ let size = CGSize(width: availableSize.width, height: contentHeight)
+ self.container.update(size: size, contextOptions: component.contextOptions, transition: transition)
+ transition.setFrame(view: self.container, frame: CGRect(origin: CGPoint(), size: size))
+ transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size))
+
+ return size
}
}
diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
index f0784bd4a2..0744f141da 100644
--- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
+++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift
@@ -425,6 +425,7 @@ public final class ListComposePollOptionComponent: Component {
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift
index 31cd8ad9a1..a7ec0fe0da 100644
--- a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift
+++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift
@@ -130,6 +130,7 @@ public final class ListItemSliderSelectorComponent: Component {
private weak var state: EmptyComponentState?
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override public init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift
index 60dc69c6bd..171a610f85 100644
--- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift
+++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift
@@ -202,6 +202,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift
index 10ad6baf5d..9b6d022b7e 100644
--- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift
+++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift
@@ -7,6 +7,7 @@ import DynamicCornerRadiusView
public protocol ListSectionComponentChildView: AnyObject {
var customUpdateIsHighlighted: ((Bool) -> Void)? { get set }
+ var enumerateSiblings: (((UIView) -> Void) -> Void)? { get set }
var separatorInset: CGFloat { get }
}
@@ -190,6 +191,16 @@ public final class ListSectionContentView: UIView {
}
self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil)
}
+ itemComponentView.enumerateSiblings = { [weak self, weak itemComponentView] f in
+ guard let self, let itemComponentView else {
+ return
+ }
+ for (_, itemView) in self.itemViews {
+ if let otherItemView = itemView.contents.view, otherItemView !== itemComponentView {
+ f(otherItemView)
+ }
+ }
+ }
}
}
var separatorInset: CGFloat = 0.0
@@ -619,6 +630,7 @@ public final class ListSubSectionComponent: Component {
private var component: ListSubSectionComponent?
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift
index ae8554b1e3..dbe60f2178 100644
--- a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift
+++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift
@@ -127,6 +127,7 @@ public final class ListTextFieldItemComponent: Component {
}
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
public override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD
index 814fcb4edd..448ecd6c1b 100644
--- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD
+++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD
@@ -68,6 +68,8 @@ swift_library(
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
"//submodules/TelegramNotices",
+ "//submodules/TelegramUI/Components/AttachmentFileController",
+ "//submodules/SaveToCameraRoll",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift
index 71d2a6fedd..1b649ed216 100644
--- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift
+++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift
@@ -50,6 +50,8 @@ import UIKitRuntimeUtils
import ImageObjectSeparation
import SaveProgressScreen
import TelegramNotices
+import AttachmentFileController
+import SaveToCameraRoll
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@@ -3732,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
} else if case let .gift(gift) = subject {
isGift = true
- let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false))]
+ let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages = .single([message])
} else {
@@ -5107,8 +5109,65 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
controller.push(locationController)
}
-
+
func presentAudioPicker() {
+ guard let controller = self.controller else {
+ return
+ }
+
+ let audioController = storyAudioPickerController(
+ context: self.context,
+ selectFromFiles: { [weak self] in
+ self?.presentAudioFilePicker()
+ },
+ dismissed: { [weak self] in
+ if let self {
+ self.mediaEditor?.play()
+ }
+ },
+ completion: { [weak self] file in
+ guard let self else {
+ return
+ }
+ let _ = (fetchMediaData(
+ context: self.context,
+ postbox: self.context.account.postbox,
+ userLocation: .other,
+ mediaReference: file
+ ) |> deliverOnMainQueue).start(next: { [weak self] state, _ in
+ guard let self else {
+ return
+ }
+ if case let .data(data) = state {
+ let path = data.path
+
+ try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
+
+ var originalFileName: String = "audio_\(Int.random(in: 0 ..< .max)).mp3"
+ if let file = file.media as? TelegramMediaFile {
+ originalFileName = file.fileName ?? "\(file.fileId.id).mp3"
+ }
+
+ let fileName = "audio_\(originalFileName)"
+ let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName)
+
+ try? FileManager.default.removeItem(atPath: copyPath)
+ do {
+ try FileManager.default.copyItem(atPath: path, toPath: copyPath)
+ } catch let e {
+ Logger.shared.log("MediaEditor", "copy file error \(e)")
+ return
+ }
+
+ self.insertAudio(path: copyPath, fileName: fileName)
+ }
+ })
+ }
+ )
+ controller.push(audioController)
+ }
+
+ func presentAudioFilePicker() {
var isSettingTrack = false
self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"], forceDarkTheme: true, dismissed: { [weak self] in
if let self {
@@ -5119,7 +5178,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
}
}, completion: { [weak self] urls in
- guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else {
+ guard let self, !urls.isEmpty, let url = urls.first else {
return
}
isSettingTrack = true
@@ -5132,7 +5191,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let coordinator = NSFileCoordinator(filePresenter: nil)
var error: NSError?
coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { sourceUrl in
- let fileName = "audio_\(sourceUrl.lastPathComponent)"
+ let fileName = "audio_\(sourceUrl.lastPathComponent)"
let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName)
try? FileManager.default.removeItem(atPath: copyPath)
@@ -5147,92 +5206,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
Queue.mainQueue().async {
- let audioAsset = AVURLAsset(url: URL(fileURLWithPath: copyPath))
-
- func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) {
- asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: {
- if asset.statusOfValue(forKey: "tracks", error: nil) == .loading {
- if retryCount < 2 {
- Queue.mainQueue().after(0.1, {
- loadValues(asset: asset, retryCount: retryCount + 1, completion: completion)
- })
- } else {
- completion()
- }
- } else {
- completion()
- }
- })
- }
-
- loadValues(asset: audioAsset, retryCount: 0, completion: {
- var audioDuration: Double = 0.0
- guard let track = audioAsset.tracks(withMediaType: .audio).first else {
- Logger.shared.log("MediaEditor", "track is nil")
- if isScopedResource {
- url.stopAccessingSecurityScopedResource()
- }
- return
- }
-
- audioDuration = track.timeRange.duration.seconds
- if audioDuration.isZero {
- Logger.shared.log("MediaEditor", "duration is zero")
- if isScopedResource {
- url.stopAccessingSecurityScopedResource()
- }
- return
- }
-
- func maybeFixMisencodedText(_ text: String) -> String {
- let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ")
- if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil {
- if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) {
- return string
- } else {
- return text
- }
- } else {
- return text
- }
- }
-
- var artist: String?
- var title: String?
- for data in audioAsset.commonMetadata {
- if data.commonKey == .commonKeyArtist, let value = data.stringValue {
- artist = maybeFixMisencodedText(value)
- }
- if data.commonKey == .commonKeyTitle, let value = data.stringValue {
- title = maybeFixMisencodedText(value)
- }
- }
-
- Queue.mainQueue().async {
- var audioTrimRange: Range?
- var audioOffset: Double?
-
- if let videoDuration = mediaEditor.originalCappedDuration {
- if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound {
- audioOffset = -videoStart
- } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound {
- audioOffset = -videoStart
- }
- audioTrimRange = 0 ..< min(videoDuration, audioDuration)
- } else {
- audioTrimRange = 0 ..< min(15, audioDuration)
- }
-
- mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset)
-
- mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true)
-
- self.requestUpdate(transition: .easeInOut(duration: 0.2))
- if isScopedResource {
- url.stopAccessingSecurityScopedResource()
- }
-
- mediaEditor.play()
+ self.insertAudio(path: copyPath, fileName: fileName, dispose: {
+ if isScopedResource {
+ url.stopAccessingSecurityScopedResource()
}
})
}
@@ -5244,6 +5220,94 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}), in: .window(.root))
}
+ private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) {
+ guard let mediaEditor = self.mediaEditor else {
+ return
+ }
+ let audioAsset = AVURLAsset(url: URL(fileURLWithPath: path))
+
+ func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) {
+ asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: {
+ if asset.statusOfValue(forKey: "tracks", error: nil) == .loading {
+ if retryCount < 2 {
+ Queue.mainQueue().after(0.1, {
+ loadValues(asset: asset, retryCount: retryCount + 1, completion: completion)
+ })
+ } else {
+ completion()
+ }
+ } else {
+ completion()
+ }
+ })
+ }
+
+ loadValues(asset: audioAsset, retryCount: 0, completion: {
+ var audioDuration: Double = 0.0
+ guard let track = audioAsset.tracks(withMediaType: .audio).first else {
+ Logger.shared.log("MediaEditor", "track is nil")
+ dispose?()
+ return
+ }
+
+ audioDuration = track.timeRange.duration.seconds
+ if audioDuration.isZero {
+ Logger.shared.log("MediaEditor", "duration is zero")
+ dispose?()
+ return
+ }
+
+ func maybeFixMisencodedText(_ text: String) -> String {
+ let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ")
+ if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil {
+ if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) {
+ return string
+ } else {
+ return text
+ }
+ } else {
+ return text
+ }
+ }
+
+ var artist: String?
+ var title: String?
+ for data in audioAsset.commonMetadata {
+ if data.commonKey == .commonKeyArtist, let value = data.stringValue {
+ artist = maybeFixMisencodedText(value)
+ }
+ if data.commonKey == .commonKeyTitle, let value = data.stringValue {
+ title = maybeFixMisencodedText(value)
+ }
+ }
+
+ Queue.mainQueue().async {
+ var audioTrimRange: Range?
+ var audioOffset: Double?
+
+ if let videoDuration = mediaEditor.originalCappedDuration {
+ if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound {
+ audioOffset = -videoStart
+ } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound {
+ audioOffset = -videoStart
+ }
+ audioTrimRange = 0 ..< min(videoDuration, audioDuration)
+ } else {
+ audioTrimRange = 0 ..< min(15, audioDuration)
+ }
+
+ mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset)
+
+ mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true)
+
+ self.requestUpdate(transition: .easeInOut(duration: 0.2))
+ dispose?()
+
+ mediaEditor.play()
+ }
+ })
+ }
+
func presentVideoRemoveConfirmation() {
guard let controller = self.controller else {
return
diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift
index e51ab6cfea..13cfb235cb 100644
--- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift
+++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift
@@ -196,6 +196,7 @@ public final class HashtagListItemComponent: Component {
private var isExtractedToContextMenu: Bool = false
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift
index b6c75199ab..0940ba237c 100644
--- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/AffiliateProgramSetupScreen.swift
@@ -858,7 +858,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(FilledRoundedRectangleComponent(
color: environment.theme.list.itemBlocksBackgroundColor,
- cornerRadius: .value(11.0),
+ cornerRadius: .value(26.0),
smoothCorners: false
)),
environment: {},
@@ -882,6 +882,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.AffiliateSetup_SectionCommission,
@@ -952,6 +953,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -1020,6 +1022,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -1032,6 +1035,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
+ style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -1070,11 +1074,13 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
+ style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -1365,6 +1371,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.AffiliateSetup_ConnectedSectionTitle,
@@ -1519,6 +1526,7 @@ final class AffiliateProgramSetupScreenComponent: Component {
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
+ style: .glass,
header: AnyComponent(HStack(suggestedHeaderItems, spacing: 4.0, alignment: .alternatingLeftRight)),
footer: nil,
items: suggestedSectionItems,
diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift
index a9ed9eff37..d8d473f822 100644
--- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift
@@ -1169,10 +1169,12 @@ private final class JoinAffiliateProgramScreenComponent: Component {
case .active:
actionButtonTitle = environment.strings.AffiliateProgram_ActionCopyLink
}
+
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
+ style: .glass,
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
@@ -1206,7 +1208,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
}
)),
environment: {},
- containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
+ containerSize: CGSize(width: availableSize.width - 30.0 * 2.0, height: 52.0)
)
let bottomTextSize = self.bottomText.update(
diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift
index 1f20c7b4b9..dd685d0439 100644
--- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift
@@ -100,9 +100,10 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
self.addSubnode(itemNode)
}
- let height = itemNode.contentSize.height
+ let verticalInset: CGFloat = 8.0
+ let height = itemNode.contentSize.height + verticalInset * 2.0
- transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
+ transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalInset), size: itemNode.bounds.size))
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
@@ -115,7 +116,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
- self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
+ self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift
index 2ee1e33879..be77701a7b 100644
--- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift
+++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift
@@ -348,7 +348,7 @@ final class PostSuggestionsSettingsScreenComponent: Component {
var contentSectionItems: [AnyComponentWithIdentity] = []
let usdRate = Double(component.usdWithdrawRate) / 1000.0 / 100.0
- let price = self.starCount == 0 ? "" : "≈\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
+ let price = self.starCount == 0 ? "" : "~\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))"
contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: MessagePriceItem(
diff --git a/submodules/TelegramUI/Components/SegmentControlComponent/BUILD b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD
new file mode 100644
index 0000000000..b4d37a733c
--- /dev/null
+++ b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD
@@ -0,0 +1,23 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "SegmentControlComponent",
+ module_name = "SegmentControlComponent",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/AsyncDisplayKit",
+ "//submodules/Display",
+ "//submodules/TelegramPresentationData",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ComponentDisplayAdapters",
+ "//submodules/SegmentedControlNode",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift
similarity index 92%
rename from submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift
rename to submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift
index 4f2cb4878b..69d6cb6853 100644
--- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift
+++ b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift
@@ -7,10 +7,15 @@ import ComponentDisplayAdapters
import TelegramPresentationData
import SegmentedControlNode
-final class SegmentControlComponent: Component {
- struct Item: Equatable {
+public final class SegmentControlComponent: Component {
+ public struct Item: Equatable {
var id: AnyHashable
var title: String
+
+ public init(id: AnyHashable, title: String) {
+ self.id = id
+ self.title = title
+ }
}
let theme: PresentationTheme
@@ -18,7 +23,7 @@ final class SegmentControlComponent: Component {
let selectedId: AnyHashable?
let action: (AnyHashable) -> Void
- init(
+ public init(
theme: PresentationTheme,
items: [Item],
selectedId: AnyHashable?,
@@ -30,7 +35,7 @@ final class SegmentControlComponent: Component {
self.action = action
}
- static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool {
+ public static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@@ -80,7 +85,7 @@ final class SegmentControlComponent: Component {
}
}
- class View: UIView {
+ public class View: UIView {
private let title = ComponentView()
private var component: SegmentControlComponent?
@@ -88,7 +93,7 @@ final class SegmentControlComponent: Component {
private var nativeSegmentedView: SegmentedControlView?
private var legacySegmentedNode: SegmentedControlNode?
- override init(frame: CGRect) {
+ public override init(frame: CGRect) {
super.init(frame: frame)
}
@@ -171,11 +176,11 @@ final class SegmentControlComponent: Component {
}
}
- func makeView() -> View {
+ public func makeView() -> View {
return View(frame: CGRect())
}
- func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift
index c293113855..74bcf76ef4 100644
--- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift
@@ -72,6 +72,7 @@ final class GreetingMessageListItemComponent: Component {
private var itemNode: ListViewItemNode?
var customUpdateIsHighlighted: ((Bool) -> Void)?
+ var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift
index 16d7b4488f..e1f7fd4b1f 100644
--- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift
@@ -73,6 +73,7 @@ final class BusinessLinkListItemComponent: Component {
private weak var componentState: EmptyComponentState?
var customUpdateIsHighlighted: ((Bool) -> Void)?
+ var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
private var isExtractedToContextMenu: Bool = false
diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift
index a6811b04db..1bd282be5d 100644
--- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift
@@ -67,6 +67,7 @@ final class ChatIntroItemComponent: Component {
private var emptyNode: ChatEmptyNode?
var customUpdateIsHighlighted: ((Bool) -> Void)?
+ var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift
index b48375c117..35f16ff3bd 100644
--- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift
@@ -56,6 +56,7 @@ final class MapPreviewComponent: Component {
private let pinForegroundView: UIImageView
var customUpdateIsHighlighted: ((Bool) -> Void)?
+ var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift
index 3423eb4caf..bbe37bf01b 100644
--- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift
+++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift
@@ -74,6 +74,7 @@ final class ChatbotSearchResultItemComponent: Component {
private weak var state: EmptyComponentState?
var customUpdateIsHighlighted: ((Bool) -> Void)?
+ var enumerateSiblings: (((UIView) -> Void) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD
new file mode 100644
index 0000000000..38a7d38057
--- /dev/null
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD
@@ -0,0 +1,37 @@
+load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
+
+swift_library(
+ name = "PasskeysScreen",
+ module_name = "PasskeysScreen",
+ srcs = glob([
+ "Sources/**/*.swift",
+ ]),
+ copts = [
+ "-warnings-as-errors",
+ ],
+ deps = [
+ "//submodules/Display",
+ "//submodules/Postbox",
+ "//submodules/TelegramCore",
+ "//submodules/SSignalKit/SwiftSignalKit",
+ "//submodules/ComponentFlow",
+ "//submodules/Components/ViewControllerComponent",
+ "//submodules/Components/ComponentDisplayAdapters",
+ "//submodules/Components/MultilineTextComponent",
+ "//submodules/TelegramPresentationData",
+ "//submodules/AccountContext",
+ "//submodules/AppBundle",
+ "//submodules/PresentationDataUtils",
+ "//submodules/Components/BundleIconComponent",
+ "//submodules/TelegramUI/Components/ButtonComponent",
+ "//submodules/Components/BalancedTextComponent",
+ "//submodules/Markdown",
+ "//submodules/TelegramUI/Components/LottieComponent",
+ "//submodules/TelegramUI/Components/ListSectionComponent",
+ "//submodules/TelegramUI/Components/ListActionItemComponent",
+ "//submodules/TelegramUI/Components/EmojiStatusComponent",
+ ],
+ visibility = [
+ "//visibility:public",
+ ],
+)
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift
new file mode 100644
index 0000000000..ba88ee22dc
--- /dev/null
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift
@@ -0,0 +1,406 @@
+import Foundation
+import UIKit
+import Display
+import ComponentFlow
+import SwiftSignalKit
+import TelegramCore
+import TelegramPresentationData
+import TelegramStringFormatting
+import ViewControllerComponent
+import BundleIconComponent
+import BalancedTextComponent
+import MultilineTextComponent
+import ButtonComponent
+import AccountContext
+import AuthenticationServices
+import PresentationDataUtils
+
+final class PasskeysScreenComponent: Component {
+ typealias EnvironmentType = ViewControllerComponentContainer.Environment
+
+ let context: AccountContext
+ let displaySkip: Bool
+ let initialPasskeysData: [TelegramPasskey]?
+ let passkeysDataUpdated: ([TelegramPasskey]) -> Void
+ let completion: () -> Void
+ let cancel: () -> Void
+
+ init(
+ context: AccountContext,
+ displaySkip: Bool,
+ initialPasskeysData: [TelegramPasskey]?,
+ passkeysDataUpdated: @escaping ([TelegramPasskey]) -> Void,
+ completion: @escaping () -> Void,
+ cancel: @escaping () -> Void
+ ) {
+ self.context = context
+ self.displaySkip = displaySkip
+ self.initialPasskeysData = initialPasskeysData
+ self.passkeysDataUpdated = passkeysDataUpdated
+ self.completion = completion
+ self.cancel = cancel
+ }
+
+ static func ==(lhs: PasskeysScreenComponent, rhs: PasskeysScreenComponent) -> Bool {
+ return true
+ }
+
+ class View: UIView, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
+ private var intro: ComponentView?
+ private var list: ComponentView?
+ private var activityIndicator: UIActivityIndicatorView?
+
+ private var component: PasskeysScreenComponent?
+ private var environment: EnvironmentType?
+ private weak var state: EmptyComponentState?
+
+ private var passkeysData: [TelegramPasskey]?
+ private var loadPasskeysDataDisposable: Disposable?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ deinit {
+ self.loadPasskeysDataDisposable?.dispose()
+ }
+
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
+ Task { @MainActor [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+
+ let encodeBase64URL: (Data) -> String = { data in
+ var string = data.base64EncodedString()
+ string = string
+ .replacingOccurrences(of: "+", with: "-")
+ .replacingOccurrences(of: "/", with: "_")
+ string = string.replacingOccurrences(of: "=", with: "")
+ return string
+ }
+
+ if #available(iOS 17.0, *) {
+ if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
+ if let clientData = String(data: credential.rawClientDataJSON, encoding: .utf8), let attestationObject = credential.rawAttestationObject {
+ let passkey = await component.context.engine.auth.requestCreatePasskey(id: encodeBase64URL(credential.credentialID), clientData: clientData, attestationObject: attestationObject).get()
+ if let passkey {
+ if self.passkeysData == nil {
+ self.passkeysData = []
+ }
+ self.passkeysData?.insert(passkey, at: 0)
+ component.passkeysDataUpdated(self.passkeysData ?? [])
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
+
+ }
+
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
+ guard let windowScene = self.window?.windowScene else {
+ preconditionFailure()
+ }
+ return ASPresentationAnchor(windowScene: windowScene)
+ }
+
+ private func createPasskey() {
+ if #available(iOS 15.0, *) {
+ Task { @MainActor [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+
+ let decodeBase64: (String) -> Data? = { string in
+ var string = string.replacingOccurrences(of: "-", with: "+")
+ .replacingOccurrences(of: "_", with: "/")
+ while string.count % 4 != 0 {
+ string.append("=")
+ }
+ return Data(base64Encoded: string)
+ }
+
+ guard let registrationData = await component.context.engine.auth.requestPasskeyRegistration().get()?.data(using: .utf8) else {
+ return
+ }
+ guard let params = try? JSONSerialization.jsonObject(with: registrationData) as? [String: Any] else {
+ return
+ }
+ guard let pkDict = params["publicKey"] as? [String: Any] else {
+ return
+ }
+ guard let rp = pkDict["rp"] as? [String: Any] else {
+ return
+ }
+ guard let relyingPartyIdentifier = rp["id"] as? String else {
+ return
+ }
+ guard let challengeBase64 = pkDict["challenge"] as? String else {
+ return
+ }
+ guard let challengeData = decodeBase64(challengeBase64) else {
+ return
+ }
+ guard let user = pkDict["user"] as? [String: Any] else {
+ return
+ }
+ guard let userIdData = user["id"] as? String else {
+ return
+ }
+ guard let userId = decodeBase64(userIdData) else {
+ return
+ }
+ guard let userName = user["name"] as? String else {
+ return
+ }
+
+ let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
+ let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challengeData, name: userName, userID: userId)
+ let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
+ authController.delegate = self
+ authController.presentationContextProvider = self
+ authController.performRequests()
+
+ component.completion()
+ }
+ }
+ }
+
+ private func displayDeletePasskey(id: String) {
+ guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
+ return
+ }
+ let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
+ controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: environment.strings.Passkeys_DeleteAlert_Title, text: environment.strings.Passkeys_DeleteAlert_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {
+ }), TextAlertAction(type: .destructiveAction, title: environment.strings.Passkeys_DeleteAlert_Action, action: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.deletePasskey(id: id)
+ })]), in: .window(.root))
+ }
+
+ private func deletePasskey(id: String) {
+ guard let component = self.component else {
+ return
+ }
+ guard let passkey = self.passkeysData?.first(where: { $0.id == id }) else {
+ return
+ }
+ let _ = component.context.engine.auth.deletePasskey(id: id).startStandalone()
+
+ self.passkeysData?.removeAll(where: { $0.id == id })
+ component.passkeysDataUpdated(self.passkeysData ?? [])
+ self.state?.updated(transition: .spring(duration: 0.4))
+
+ if #available(iOS 26.0, *) {
+ Task { @MainActor in
+ let updater = ASCredentialUpdater()
+ let decodeBase64: (String) -> Data? = { string in
+ var string = string.replacingOccurrences(of: "-", with: "+")
+ .replacingOccurrences(of: "_", with: "/")
+ while string.count % 4 != 0 {
+ string.append("=")
+ }
+ return Data(base64Encoded: string)
+ }
+ if let credentialId = decodeBase64(passkey.id) {
+ do {
+ try await updater.reportUnknownPublicKeyCredential(relyingPartyIdentifier: "telegram.org", credentialID: credentialId)
+ } catch let e {
+ Logger.shared.log("Passkeys", "reportUnknownPublicKeyCredential error: \(e)")
+ }
+ }
+ }
+ }
+ }
+
+ func update(component: PasskeysScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
+
+ if self.component == nil {
+ self.passkeysData = component.initialPasskeysData
+ if self.passkeysData == nil {
+ self.loadPasskeysDataDisposable = (component.context.engine.auth.passkeysData()
+ |> take(1)
+ |> deliverOnMainQueue).startStrict(next: { [weak self] data in
+ guard let self, let component = self.component else {
+ return
+ }
+ self.passkeysData = data
+ component.passkeysDataUpdated(data)
+ self.state?.updated(transition: .easeInOut(duration: 0.25))
+ })
+ }
+ }
+
+ self.component = component
+ let environment = environment[ViewControllerComponentContainer.Environment.self].value
+ self.environment = environment
+ self.state = state
+
+ self.backgroundColor = environment.theme.list.plainBackgroundColor
+
+ if let passkeysData = self.passkeysData, passkeysData.isEmpty {
+ let intro: ComponentView
+ var introTransition = transition
+ if let current = self.intro {
+ intro = current
+ } else {
+ introTransition = transition.withAnimation(.none)
+ intro = ComponentView()
+ self.intro = intro
+ }
+ let _ = intro.update(
+ transition: introTransition,
+ component: AnyComponent(PasskeysScreenIntroComponent(
+ context: component.context,
+ theme: environment.theme,
+ strings: environment.strings,
+ insets: UIEdgeInsets(top: environment.statusBarHeight + environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0),
+ displaySkip: component.displaySkip,
+ createPasskeyAction: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.createPasskey()
+ },
+ skipAction: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+ component.cancel()
+ self.environment?.controller()?.dismiss()
+ }
+ )),
+ environment: {},
+ containerSize: availableSize
+ )
+ if let introView = intro.view {
+ if introView.superview == nil {
+ self.addSubview(introView)
+ introView.alpha = 0.0
+ }
+ transition.setFrame(view: introView, frame: CGRect(origin: CGPoint(), size: availableSize))
+ alphaTransition.setAlpha(view: introView, alpha: 1.0)
+ }
+ } else {
+ if let intro = self.intro {
+ self.intro = nil
+ if let introView = intro.view {
+ alphaTransition.setAlpha(view: introView, alpha: 0.0, completion: { [weak introView] _ in
+ introView?.removeFromSuperview()
+ })
+ }
+ }
+ }
+
+ if let passkeysData = self.passkeysData, !passkeysData.isEmpty {
+ let list: ComponentView
+ var listTransition = transition
+ if let current = self.list {
+ list = current
+ } else {
+ listTransition = transition.withAnimation(.none)
+ list = ComponentView()
+ self.list = list
+ }
+ let _ = list.update(
+ transition: listTransition,
+ component: AnyComponent(PasskeysScreenListComponent(
+ context: component.context,
+ theme: environment.theme,
+ strings: environment.strings,
+ insets: UIEdgeInsets(top: environment.statusBarHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0),
+ passkeys: passkeysData,
+ addPasskeyAction: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.createPasskey()
+ },
+ deletePasskeyAction: { [weak self] id in
+ guard let self else {
+ return
+ }
+ self.displayDeletePasskey(id: id)
+ }
+ )),
+ environment: {},
+ containerSize: availableSize
+ )
+ if let listView = list.view {
+ if listView.superview == nil {
+ self.addSubview(listView)
+ listView.alpha = 0.0
+ }
+ transition.setFrame(view: listView, frame: CGRect(origin: CGPoint(), size: availableSize))
+ alphaTransition.setAlpha(view: listView, alpha: 1.0)
+ }
+ } else {
+ if let list = self.list {
+ self.list = nil
+ if let listView = list.view {
+ alphaTransition.setAlpha(view: listView, alpha: 0.0, completion: { [weak listView] _ in
+ listView?.removeFromSuperview()
+ })
+ }
+ }
+ }
+
+ if self.passkeysData == nil {
+ let activityIndicator: UIActivityIndicatorView
+ if let current = self.activityIndicator {
+ activityIndicator = current
+ } else {
+ activityIndicator = UIActivityIndicatorView(style: .large)
+ self.activityIndicator = activityIndicator
+ self.addSubview(activityIndicator)
+ }
+ activityIndicator.tintColor = environment.theme.list.itemPrimaryTextColor
+
+ let indicatorSize = activityIndicator.bounds.size
+ activityIndicator.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - indicatorSize.width) / 2.0), y: floor((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)
+ if !activityIndicator.isAnimating {
+ activityIndicator.startAnimating()
+ }
+ } else if let activityIndicator = self.activityIndicator {
+ self.activityIndicator = nil
+ activityIndicator.removeFromSuperview()
+ }
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+public final class PasskeysScreen: ViewControllerComponentContainer {
+ private let context: AccountContext
+
+ public init(context: AccountContext, displaySkip: Bool, initialPasskeysData: [TelegramPasskey]?, passkeysDataUpdated: @escaping ([TelegramPasskey]) -> Void, completion: @escaping () -> Void, cancel: @escaping () -> Void) {
+ self.context = context
+
+ super.init(context: context, component: PasskeysScreenComponent(context: context, displaySkip: displaySkip, initialPasskeysData: initialPasskeysData, passkeysDataUpdated: passkeysDataUpdated, completion: completion, cancel: cancel), navigationBarAppearance: .transparent)
+ }
+
+ required public init(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift
new file mode 100644
index 0000000000..5a63cb70e6
--- /dev/null
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift
@@ -0,0 +1,394 @@
+import Foundation
+import UIKit
+import Display
+import ComponentFlow
+import TelegramPresentationData
+import AccountContext
+import LottieComponent
+import MultilineTextComponent
+import BalancedTextComponent
+import ButtonComponent
+import BundleIconComponent
+
+final class PasskeysScreenIntroComponent: Component {
+ let context: AccountContext
+ let theme: PresentationTheme
+ let strings: PresentationStrings
+ let insets: UIEdgeInsets
+ let displaySkip: Bool
+ let createPasskeyAction: () -> Void
+ let skipAction: () -> Void
+
+ init(
+ context: AccountContext,
+ theme: PresentationTheme,
+ strings: PresentationStrings,
+ insets: UIEdgeInsets,
+ displaySkip: Bool,
+ createPasskeyAction: @escaping () -> Void,
+ skipAction: @escaping () -> Void
+ ) {
+ self.context = context
+ self.theme = theme
+ self.strings = strings
+ self.insets = insets
+ self.displaySkip = displaySkip
+ self.createPasskeyAction = createPasskeyAction
+ self.skipAction = skipAction
+ }
+
+ static func ==(lhs: PasskeysScreenIntroComponent, rhs: PasskeysScreenIntroComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.strings !== rhs.strings {
+ return false
+ }
+ if lhs.insets != rhs.insets {
+ return false
+ }
+ if lhs.displaySkip != rhs.displaySkip {
+ return false
+ }
+ return true
+ }
+
+ private final class ScrollViewImpl: UIScrollView {
+ override func touchesShouldCancel(in view: UIView) -> Bool {
+ return true
+ }
+ }
+
+ private final class Item {
+ let icon = ComponentView()
+ let title = ComponentView()
+ let text = ComponentView()
+
+ init() {
+ }
+ }
+
+ class View: UIView, UIScrollViewDelegate {
+ private let scrollView: ScrollViewImpl
+ private let contentContainer: UIView
+
+ private let icon = ComponentView()
+ private let title = ComponentView()
+ private let subtitle = ComponentView()
+ private let actionButton = ComponentView()
+ private var skipButton: ComponentView?
+
+ private var items: [Item] = []
+
+ private var component: PasskeysScreenIntroComponent?
+ private weak var state: EmptyComponentState?
+
+ override init(frame: CGRect) {
+ self.scrollView = ScrollViewImpl()
+ self.contentContainer = UIView()
+ self.scrollView.addSubview(self.contentContainer)
+
+ super.init(frame: frame)
+
+ self.scrollView.delaysContentTouches = true
+ self.scrollView.canCancelContentTouches = true
+ self.scrollView.clipsToBounds = false
+ self.scrollView.contentInsetAdjustmentBehavior = .never
+ self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
+ self.scrollView.showsVerticalScrollIndicator = false
+ self.scrollView.showsHorizontalScrollIndicator = false
+ self.scrollView.alwaysBounceHorizontal = false
+ self.scrollView.scrollsToTop = false
+ self.scrollView.delegate = self
+ self.scrollView.clipsToBounds = true
+ self.addSubview(self.scrollView)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ deinit {
+ }
+
+ func update(component: PasskeysScreenIntroComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+ self.state = state
+
+ self.backgroundColor = component.theme.list.plainBackgroundColor
+
+ let sideInset: CGFloat = 16.0
+ let sideIconInset: CGFloat = 40.0
+ let itemsSideInset: CGFloat = sideInset + 20.0
+
+ var contentHeight: CGFloat = 0.0
+
+ let iconSize = self.icon.update(
+ transition: .immediate,
+ component: AnyComponent(LottieComponent(
+ content: LottieComponent.AppBundleContent(name: "passkey_logo"),
+ loop: false
+ )),
+ environment: {},
+ containerSize: CGSize(width: 124.0, height: 124.0)
+ )
+ let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight), size: iconSize)
+ if let iconView = self.icon.view as? LottieComponent.View {
+ if iconView.superview == nil {
+ self.contentContainer.addSubview(iconView)
+ iconView.playOnce()
+ }
+ transition.setPosition(view: iconView, position: iconFrame.center)
+ iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
+ }
+ contentHeight += iconSize.height
+ contentHeight += 10.0
+
+ let titleSize = self.title.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: component.strings.Passkeys_Into_Title, font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.contentContainer.addSubview(titleView)
+ }
+ transition.setPosition(view: titleView, position: titleFrame.center)
+ titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
+ }
+ contentHeight += titleSize.height
+ contentHeight += 10.0
+
+ let subtitleSize = self.subtitle.update(
+ transition: .immediate,
+ component: AnyComponent(BalancedTextComponent(
+ text: .plain(NSAttributedString(string: component.strings.Passkeys_Subtitle, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.2
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
+ )
+ let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)
+ if let subtitleView = self.subtitle.view {
+ if subtitleView.superview == nil {
+ self.contentContainer.addSubview(subtitleView)
+ }
+ transition.setPosition(view: subtitleView, position: subtitleFrame.center)
+ subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
+ }
+ contentHeight += subtitleSize.height
+ contentHeight += 47.0
+
+ struct ItemDesc {
+ var icon: String
+ var title: String
+ var text: String
+ }
+ let itemDescs: [ItemDesc] = [
+ ItemDesc(
+ icon: "Settings/Passkeys/Intro1",
+ title: component.strings.Passkeys_Into_Title0,
+ text: component.strings.Passkeys_Into_Text0
+ ),
+ ItemDesc(
+ icon: "Settings/Passkeys/Intro2",
+ title: component.strings.Passkeys_Into_Title1,
+ text: component.strings.Passkeys_Into_Text1
+ ),
+ ItemDesc(
+ icon: "Settings/Passkeys/Intro3",
+ title: component.strings.Passkeys_Into_Title2,
+ text: component.strings.Passkeys_Into_Text2
+ )
+ ]
+ for i in 0 ..< itemDescs.count {
+ if i != 0 {
+ contentHeight += 24.0
+ }
+
+ let item: Item
+ if self.items.count > i {
+ item = self.items[i]
+ } else {
+ item = Item()
+ self.items.append(item)
+ }
+
+ let itemDesc = itemDescs[i]
+
+ let iconSize = item.icon.update(
+ transition: .immediate,
+ component: AnyComponent(BundleIconComponent(
+ name: itemDesc.icon,
+ tintColor: component.theme.list.itemAccentColor
+ )),
+ environment: {},
+ containerSize: CGSize(width: 100.0, height: 100.0)
+ )
+ let titleSize = item.title.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)),
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.2
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - itemsSideInset * 2.0 - sideIconInset, height: 1000.0)
+ )
+ let textSize = item.text.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)),
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.18
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - itemsSideInset * 2.0 - sideIconInset, height: 1000.0)
+ )
+
+ if let iconView = item.icon.view {
+ if iconView.superview == nil {
+ self.contentContainer.addSubview(iconView)
+ }
+ transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: itemsSideInset, y: contentHeight + 4.0), size: iconSize))
+ }
+
+ if let titleView = item.title.view {
+ if titleView.superview == nil {
+ self.contentContainer.addSubview(titleView)
+ }
+ transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: itemsSideInset + sideIconInset, y: contentHeight), size: titleSize))
+ }
+ contentHeight += titleSize.height
+ contentHeight += 2.0
+
+ if let textView = item.text.view {
+ if textView.superview == nil {
+ self.contentContainer.addSubview(textView)
+ }
+ transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: itemsSideInset + sideIconInset, y: contentHeight), size: textSize))
+ }
+ contentHeight += textSize.height
+ }
+
+ let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: component.insets.bottom, innerDiameter: 52.0, sideInset: 32.0)
+ let actionButtonSize = self.actionButton.update(
+ transition: transition,
+ component: AnyComponent(ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: component.theme.list.itemCheckColors.fillColor,
+ foreground: component.theme.list.itemCheckColors.foregroundColor,
+ pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
+ cornerRadius: 52.0 * 0.5
+ ),
+ content: AnyComponentWithIdentity(
+ id: AnyHashable(0),
+ component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Passkeys_ButtonCreate, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor))))
+ ),
+ action: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+ component.createPasskeyAction()
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
+ )
+
+ var skipButtonSize: CGSize?
+ if component.displaySkip {
+ let skipButton: ComponentView
+ if let current = self.skipButton {
+ skipButton = current
+ } else {
+ skipButton = ComponentView()
+ self.skipButton = skipButton
+ }
+ skipButtonSize = skipButton.update(
+ transition: transition,
+ component: AnyComponent(ButtonComponent(
+ background: ButtonComponent.Background(
+ style: .glass,
+ color: component.theme.chatList.unreadBadgeInactiveBackgroundColor,
+ foreground: component.theme.list.itemCheckColors.foregroundColor,
+ pressedColor: component.theme.chatList.unreadBadgeInactiveBackgroundColor.withMultipliedAlpha(0.9),
+ cornerRadius: 52.0 * 0.5
+ ),
+ content: AnyComponentWithIdentity(
+ id: AnyHashable(0),
+ component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Passkeys_ButtonSkip, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor))))
+ ),
+ action: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+ component.skipAction()
+ }
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0)
+ )
+ } else if let skipButton = self.skipButton {
+ self.skipButton = nil
+ skipButton.view?.removeFromSuperview()
+ }
+
+ var buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: availableSize.height - buttonInsets.bottom - actionButtonSize.height), size: actionButtonSize)
+ if let skipButtonSize, let skipButtonView = self.skipButton?.view {
+ let skipButtonFrame = buttonFrame
+ buttonFrame = buttonFrame.offsetBy(dx: 0.0, dy: -8.0 - skipButtonSize.height)
+
+ if skipButtonView.superview == nil {
+ self.addSubview(skipButtonView)
+ }
+ transition.setFrame(view: skipButtonView, frame: skipButtonFrame)
+ }
+
+ if let actionButtonView = self.actionButton.view {
+ if actionButtonView.superview == nil {
+ self.addSubview(actionButtonView)
+ }
+ transition.setFrame(view: actionButtonView, frame: buttonFrame)
+ }
+
+ let contentTopInset = component.insets.top
+ let contentBottomInset = availableSize.height - buttonFrame.minY
+
+ let contentContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: max(contentTopInset, floor((availableSize.height - contentTopInset - contentBottomInset - contentHeight) * 0.5))), size: CGSize(width: availableSize.width, height: contentHeight))
+ transition.setFrame(view: self.contentContainer, frame: contentContainerFrame)
+
+ self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
+ let scrollContentSize = CGSize(width: availableSize.width, height: contentContainerFrame.maxY)
+ if self.scrollView.contentSize != scrollContentSize {
+ self.scrollView.contentSize = scrollContentSize
+ }
+ let scrollInsets = UIEdgeInsets(top: component.insets.top, left: 0.0, bottom: 0.0, right: 0.0)
+ if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
+ self.scrollView.verticalScrollIndicatorInsets = scrollInsets
+ }
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift
new file mode 100644
index 0000000000..86c5dccc36
--- /dev/null
+++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift
@@ -0,0 +1,363 @@
+import Foundation
+import UIKit
+import Display
+import ComponentFlow
+import TelegramPresentationData
+import AccountContext
+import LottieComponent
+import MultilineTextComponent
+import BalancedTextComponent
+import ButtonComponent
+import BundleIconComponent
+import ListSectionComponent
+import ListActionItemComponent
+import TelegramCore
+import EmojiStatusComponent
+
+final class PasskeysScreenListComponent: Component {
+ let context: AccountContext
+ let theme: PresentationTheme
+ let strings: PresentationStrings
+ let insets: UIEdgeInsets
+ let passkeys: [TelegramPasskey]
+ let addPasskeyAction: () -> Void
+ let deletePasskeyAction: (String) -> Void
+
+ init(
+ context: AccountContext,
+ theme: PresentationTheme,
+ strings: PresentationStrings,
+ insets: UIEdgeInsets,
+ passkeys: [TelegramPasskey],
+ addPasskeyAction: @escaping () -> Void,
+ deletePasskeyAction: @escaping (String) -> Void
+ ) {
+ self.context = context
+ self.theme = theme
+ self.strings = strings
+ self.insets = insets
+ self.passkeys = passkeys
+ self.addPasskeyAction = addPasskeyAction
+ self.deletePasskeyAction = deletePasskeyAction
+ }
+
+ static func ==(lhs: PasskeysScreenListComponent, rhs: PasskeysScreenListComponent) -> Bool {
+ if lhs.context !== rhs.context {
+ return false
+ }
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.strings !== rhs.strings {
+ return false
+ }
+ if lhs.insets != rhs.insets {
+ return false
+ }
+ if lhs.passkeys != rhs.passkeys {
+ return false
+ }
+ return true
+ }
+
+ private final class ScrollViewImpl: UIScrollView {
+ override func touchesShouldCancel(in view: UIView) -> Bool {
+ return true
+ }
+ }
+
+ class View: UIView, UIScrollViewDelegate {
+ private let scrollView: ScrollViewImpl
+ private let contentContainer: UIView
+
+ private let icon = ComponentView()
+ private let title = ComponentView()
+ private let subtitle = ComponentView()
+ private let listSection = ComponentView()
+
+ private var component: PasskeysScreenListComponent?
+ private weak var state: EmptyComponentState?
+
+ override init(frame: CGRect) {
+ self.scrollView = ScrollViewImpl()
+ self.contentContainer = UIView()
+ self.scrollView.addSubview(self.contentContainer)
+
+ super.init(frame: frame)
+
+ self.scrollView.delaysContentTouches = true
+ self.scrollView.canCancelContentTouches = true
+ self.scrollView.clipsToBounds = false
+ self.scrollView.contentInsetAdjustmentBehavior = .never
+ self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
+ self.scrollView.showsVerticalScrollIndicator = false
+ self.scrollView.showsHorizontalScrollIndicator = false
+ self.scrollView.alwaysBounceHorizontal = false
+ self.scrollView.scrollsToTop = false
+ self.scrollView.delegate = self
+ self.scrollView.clipsToBounds = true
+ self.addSubview(self.scrollView)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ deinit {
+ }
+
+ func update(component: PasskeysScreenListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+ self.state = state
+
+ var maxPasskeys = 5
+ if let data = component.context.currentAppConfiguration.with({ $0 }).data, let maxValue = data["passkeys_account_passkeys_max"] as? Double {
+ maxPasskeys = Int(maxValue)
+ }
+
+ self.backgroundColor = component.theme.list.blocksBackgroundColor
+
+ let sideInset: CGFloat = 16.0 + component.insets.left
+
+ var contentHeight: CGFloat = 0.0
+ contentHeight += component.insets.top
+ contentHeight += 8.0
+
+ let iconSize = self.icon.update(
+ transition: .immediate,
+ component: AnyComponent(LottieComponent(
+ content: LottieComponent.AppBundleContent(name: "passkey_logo"),
+ loop: false
+ )),
+ environment: {},
+ containerSize: CGSize(width: 124.0, height: 124.0)
+ )
+ let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight), size: iconSize)
+ if let iconView = self.icon.view as? LottieComponent.View {
+ if iconView.superview == nil {
+ self.contentContainer.addSubview(iconView)
+ iconView.playOnce()
+ }
+ transition.setPosition(view: iconView, position: iconFrame.center)
+ iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
+ }
+ contentHeight += iconSize.height
+ contentHeight += 10.0
+
+ let titleSize = self.title.update(
+ transition: .immediate,
+ component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(string: component.strings.Passkeys_Title, font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.contentContainer.addSubview(titleView)
+ }
+ transition.setPosition(view: titleView, position: titleFrame.center)
+ titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
+ }
+ contentHeight += titleSize.height
+ contentHeight += 10.0
+
+ let subtitleSize = self.subtitle.update(
+ transition: .immediate,
+ component: AnyComponent(BalancedTextComponent(
+ text: .plain(NSAttributedString(string: component.strings.Passkeys_Subtitle, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)),
+ horizontalAlignment: .center,
+ maximumNumberOfLines: 0,
+ lineSpacing: 0.2
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
+ )
+ let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)
+ if let subtitleView = self.subtitle.view {
+ if subtitleView.superview == nil {
+ self.contentContainer.addSubview(subtitleView)
+ }
+ transition.setPosition(view: subtitleView, position: subtitleFrame.center)
+ subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
+ }
+ contentHeight += subtitleSize.height
+ contentHeight += 32.0
+
+ var listSectionItems: [AnyComponentWithIdentity] = []
+ for passkey in component.passkeys {
+ if listSectionItems.contains(where: { $0.id == AnyHashable(passkey.id) }) {
+ continue
+ }
+ let passkeyId = passkey.id
+ let dateFormatter = DateFormatter()
+ dateFormatter.timeStyle = .none
+ dateFormatter.dateStyle = .medium
+ let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(passkey.date)))
+
+ let iconComponent: AnyComponentWithIdentity
+ if let emojiId = passkey.emojiId {
+ iconComponent = AnyComponentWithIdentity(
+ id: "lottie",
+ component: AnyComponent(TransformContents(
+ content: AnyComponent(EmojiStatusComponent(
+ context: component.context,
+ animationCache: component.context.animationCache,
+ animationRenderer: component.context.animationRenderer,
+ content: .animation(
+ content: .customEmoji(fileId: emojiId),
+ size: CGSize(width: 40.0, height: 40.0),
+ placeholderColor: component.theme.list.mediaPlaceholderColor,
+ themeColor: nil,
+ loopMode: .count(1)
+ ),
+ size: CGSize(width: 40.0, height: 40.0),
+ isVisibleForAnimations: true,
+ action: nil
+ )),
+ translation: CGPoint(x: 0.0, y: 1.0)
+ ))
+ )
+ } else {
+ iconComponent = AnyComponentWithIdentity(
+ id: "icon",
+ component: AnyComponent(BundleIconComponent(name: "Settings/Menu/Passkeys", tintColor: nil))
+ )
+ }
+
+ let subtitleString: String
+ if let lastUsageDate = passkey.lastUsageDate {
+ let lastUsedDateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(lastUsageDate)))
+ subtitleString = component.strings.Passkeys_PasskeyCreatedAndUsedPattern(dateString, lastUsedDateString).string
+ } else {
+ subtitleString = component.strings.Passkeys_PasskeyCreatedPattern(dateString).string
+ }
+
+ listSectionItems.append(AnyComponentWithIdentity(id: passkey.id, component: AnyComponent(ListActionItemComponent(
+ theme: component.theme,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: passkey.name.isEmpty ? component.strings.Passkeys_EmptyName : passkey.name,
+ font: Font.regular(17.0),
+ textColor: component.theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: subtitleString,
+ font: Font.regular(14.0),
+ textColor: component.theme.list.itemSecondaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ )))
+ ], alignment: .left, spacing: 2.0)),
+ leftIcon: .custom(
+ iconComponent,
+ false
+ ),
+ accessory: nil,
+ contextOptions: [ListActionItemComponent.ContextOption(
+ id: "delete",
+ title: component.strings.Common_Delete,
+ color: component.theme.list.itemDisclosureActions.destructive.fillColor,
+ action: { [weak self] in
+ guard let self, let component = self.component else {
+ return
+ }
+ component.deletePasskeyAction(passkeyId)
+ }
+ )],
+ action: nil,
+ highlighting: .default
+ ))))
+ }
+
+ if component.passkeys.count < maxPasskeys {
+ listSectionItems.append(AnyComponentWithIdentity(id: "_add", component: AnyComponent(ListActionItemComponent(
+ theme: component.theme,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: component.strings.Passkeys_AddPasskey,
+ font: Font.regular(17.0),
+ textColor: component.theme.list.itemAccentColor
+ )),
+ maximumNumberOfLines: 1
+ )))
+ ], alignment: .left, spacing: 2.0)),
+ leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
+ name: "Chat List/AddIcon",
+ tintColor: component.theme.list.itemAccentColor
+ ))), false),
+ accessory: nil,
+ action: { [weak self] _ in
+ guard let self, let component = self.component else {
+ return
+ }
+ component.addPasskeyAction()
+ },
+ highlighting: .default
+ ))))
+ }
+
+ let listSectionSize = self.listSection.update(
+ transition: transition,
+ component: AnyComponent(ListSectionComponent(
+ theme: component.theme,
+ style: .glass,
+ header: nil,
+ footer: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: component.strings.Passkeys_ListFooter,
+ font: Font.regular(13.0),
+ textColor: component.theme.list.freeTextColor
+ )),
+ maximumNumberOfLines: 0
+ )),
+ items: listSectionItems
+ )),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
+ )
+ let listSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: listSectionSize)
+ if let listSectionView = self.listSection.view as? ListSectionComponent.View {
+ if listSectionView.superview == nil {
+ self.contentContainer.addSubview(listSectionView)
+ self.listSection.parentState = state
+ }
+ transition.setFrame(view: listSectionView, frame: listSectionFrame)
+ }
+ contentHeight += listSectionSize.height
+ contentHeight += 8.0
+ contentHeight += component.insets.bottom
+
+ let contentContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: contentHeight))
+ transition.setFrame(view: self.contentContainer, frame: contentContainerFrame)
+
+ self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
+ let scrollContentSize = CGSize(width: availableSize.width, height: contentContainerFrame.maxY)
+ if self.scrollView.contentSize != scrollContentSize {
+ self.scrollView.contentSize = scrollContentSize
+ }
+ let scrollInsets = UIEdgeInsets(top: component.insets.top, left: 0.0, bottom: 0.0, right: 0.0)
+ if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
+ self.scrollView.verticalScrollIndicatorInsets = scrollInsets
+ }
+
+ return availableSize
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift
index 20d3361b50..f3918590f6 100644
--- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift
+++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift
@@ -774,10 +774,12 @@ final class UserAppearanceScreenComponent: Component {
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil,
+ valueUsdAmount: nil,
flags: [],
themePeerId: nil,
peerColor: nil,
- hostPeerId: nil
+ hostPeerId: nil,
+ minOfferStars: nil
)
signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate)
} else {
diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift
index d30e7eac64..b0a69117a4 100644
--- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift
+++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift
@@ -253,6 +253,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
textString = strings.Stars_Purchase_BuyStarGiftInfo
case .removeOriginalDetailsStarGift:
textString = strings.Stars_Purchase_RemoveOriginalDetailsStarGiftInfo
+ case .starGiftOffer:
+ textString = strings.Stars_Purchase_StarGiftOfferInfo
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
@@ -843,7 +845,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
titleText = strings.Stars_Purchase_GetStars
case .gift:
titleText = strings.Stars_Purchase_GiftStars
- case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars), let .removeOriginalDetailsStarGift(requiredStars):
+ case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars), let .removeOriginalDetailsStarGift(requiredStars), let .starGiftOffer(requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
@@ -1312,6 +1314,10 @@ private extension StarsPurchasePurpose {
return requiredStars
case let .buyStarGift(requiredStars):
return requiredStars
+ case let .removeOriginalDetailsStarGift(requiredStars):
+ return requiredStars
+ case let .starGiftOffer(requiredStars):
+ return requiredStars
default:
return nil
}
diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift
index af4d150393..c3e484a85e 100644
--- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift
+++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift
@@ -220,7 +220,7 @@ final class StarsBalanceComponent: Component {
let subtitleText: String
if let rate = component.rate {
- subtitleText = "≈\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))"
+ subtitleText = "~\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))"
} else {
subtitleText = component.strings.Stars_Intro_YourBalance
}
diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift
index 3551b536e3..9130474ac6 100644
--- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift
+++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift
@@ -114,7 +114,7 @@ final class StarsOverviewItemComponent: Component {
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
- text: .plain(NSAttributedString(string: "≈\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
+ text: .plain(NSAttributedString(string: "~\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
)
),
environment: {},
diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD
index 573462e416..4e7000f275 100644
--- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD
+++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD
@@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
+ "//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift
index 3a3a2ee0b3..1ff5d8140d 100644
--- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift
+++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift
@@ -27,6 +27,7 @@ import TabSelectorComponent
import PresentationDataUtils
import BalanceNeededScreen
import GlassBarButtonComponent
+import GlassBackgroundComponent
private let amountTag = GenericComponentViewTag()
@@ -61,12 +62,14 @@ private final class SheetContent: CombinedComponent {
let currencyToggle = Child(TabSelectorComponent.self)
let amountSection = Child(ListSectionComponent.self)
let amountAdditionalLabel = Child(MultilineTextComponent.self)
+ let periodSection = Child(ListSectionComponent.self)
let timestampSection = Child(ListSectionComponent.self)
let onlyTonSection = Child(ListSectionComponent.self)
let button = Child(ButtonComponent.self)
let balanceTitle = Child(MultilineTextComponent.self)
let balanceValue = Child(MultilineTextComponent.self)
let balanceIcon = Child(BundleIconComponent.self)
+ let durationPicker = Child(MenuComponent.self)
let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in
let environment = context.environment[EnvironmentType.self]
@@ -177,7 +180,7 @@ private final class SheetContent: CombinedComponent {
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
- amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
}
case .reaction:
titleString = environment.strings.Stars_SendStars_Title
@@ -231,10 +234,33 @@ private final class SheetContent: CombinedComponent {
switch state.currency {
case .stars:
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
- amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
case .ton:
let usdRate = Double(tonUsdRate) / 1000.0 / 1000000.0
- amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ }
+ }
+ case let .starGiftOffer(_, gift, _):
+ titleString = environment.strings.Gift_Offer_Title
+ switch state.currency {
+ case .stars:
+ amountTitle = environment.strings.Gift_Offer_PriceSectionStars
+ minAmount = StarsAmount(value: gift.minOfferStars ?? resaleConfiguration.starGiftResaleMinStarsAmount, nanos: 0)
+ case .ton:
+ amountTitle = environment.strings.Gift_Offer_PriceSectionTon
+ minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinTonAmount, nanos: 0)
+ }
+ maxAmount = nil
+ amountPlaceholder = environment.strings.Gift_Offer_PricePlaceholder
+
+ if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let tonUsdRate = withdrawConfiguration.tonUsdRate, let amount = state.amount, amount > StarsAmount.zero {
+ switch state.currency {
+ case .stars:
+ let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
+ amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ case .ton:
+ let usdRate = Double(tonUsdRate) / 1000.0 / 1000000.0
+ amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
}
}
}
@@ -309,8 +335,12 @@ private final class SheetContent: CombinedComponent {
tonBalanceValue = tonBalance
}
- if case let .suggestedPost(mode, _, _, _) = component.mode {
- var displayCurrencySelector = false
+ let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int)
+ let starsTitle: String
+ let tonTitle: String
+ var displayCurrencySelector = false
+ switch component.mode {
+ case let .suggestedPost(mode, _, _, _):
switch mode {
case let .sender(_, isFromAdmin):
if isFromAdmin {
@@ -320,74 +350,74 @@ private final class SheetContent: CombinedComponent {
displayCurrencySelector = true
}
}
+ starsTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferStars
+ tonTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferTon
case .admin:
displayCurrencySelector = true
+ starsTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestStars
+ tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon
}
-
- if displayCurrencySelector {
- let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int)
- let starsTitle: String
- let tonTitle: String
- switch mode {
- case .sender:
- starsTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferStars
- tonTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferTon
- case .admin:
- starsTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestStars
- tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon
- }
-
- let currencyToggle = currencyToggle.update(
- component: TabSelectorComponent(
- colors: TabSelectorComponent.Colors(
- foreground: theme.list.itemSecondaryTextColor,
- selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
- simple: true
- ),
- theme: theme,
- customLayout: TabSelectorComponent.CustomLayout(
- font: Font.medium(14.0),
- spacing: 10.0
- ),
- items: [
- TabSelectorComponent.Item(
- id: AnyHashable(0),
- content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme)))
- ),
- TabSelectorComponent.Item(
- id: AnyHashable(1),
- content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme)))
- )
- ],
- selectedId: selectedId,
- setSelectedId: { [weak state] id in
- guard let state else {
- return
- }
-
- let currency: CurrencyAmount.Currency
- if id == AnyHashable(0) {
- currency = .stars
- } else {
- currency = .ton
- }
- if state.currency != currency {
- state.currency = currency
- state.amount = nil
- }
- state.updated(transition: .spring(duration: 0.4))
- }
+ case .starGiftOffer:
+ displayCurrencySelector = true
+ starsTitle = environment.strings.Gift_Offer_OfferStars
+ tonTitle = environment.strings.Gift_Offer_OfferTon
+ default:
+ starsTitle = ""
+ tonTitle = ""
+ }
+
+ if displayCurrencySelector {
+ let currencyToggle = currencyToggle.update(
+ component: TabSelectorComponent(
+ colors: TabSelectorComponent.Colors(
+ foreground: theme.list.itemSecondaryTextColor,
+ selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
+ simple: true
),
- availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0),
- transition: context.transition
- )
- contentSize.height -= 17.0
- let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size)
- context.add(currencyToggle
- .position(currencyToggle.size.centered(in: currencyToggleFrame).center))
-
- contentSize.height += currencyToggle.size.height + 29.0
- }
+ theme: theme,
+ style: .glass,
+ customLayout: TabSelectorComponent.CustomLayout(
+ font: Font.medium(14.0),
+ spacing: 10.0
+ ),
+ items: [
+ TabSelectorComponent.Item(
+ id: AnyHashable(0),
+ content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme)))
+ ),
+ TabSelectorComponent.Item(
+ id: AnyHashable(1),
+ content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme)))
+ )
+ ],
+ selectedId: selectedId,
+ setSelectedId: { [weak state] id in
+ guard let state else {
+ return
+ }
+
+ let currency: CurrencyAmount.Currency
+ if id == AnyHashable(0) {
+ currency = .stars
+ } else {
+ currency = .ton
+ }
+ if state.currency != currency {
+ state.currency = currency
+ state.amount = nil
+ }
+ state.updated(transition: .spring(duration: 0.4))
+ }
+ ),
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0),
+ transition: context.transition
+ )
+ contentSize.height -= 17.0
+ let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size)
+ context.add(currencyToggle
+ .position(currencyToggle.size.centered(in: currencyToggleFrame).center))
+
+ contentSize.height += currencyToggle.size.height + 29.0
}
let amountFont = Font.regular(13.0)
@@ -446,7 +476,7 @@ private final class SheetContent: CombinedComponent {
if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
- amountRightLabel = "≈\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
+ amountRightLabel = "~\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
}
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.starGiftCommissionStarsPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural))
@@ -458,7 +488,7 @@ private final class SheetContent: CombinedComponent {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(tonString).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
if let tonUsdRate = withdrawConfiguration.tonUsdRate {
- amountRightLabel = "≈\(formatTonUsdValue(tonValue, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))"
+ amountRightLabel = "~\(formatTonUsdValue(tonValue, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))"
}
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.starGiftCommissionTonPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural))
@@ -519,6 +549,20 @@ private final class SheetContent: CombinedComponent {
maximumNumberOfLines: 0
))
}
+ case let .starGiftOffer(_, uniqueGift, _):
+ let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))"
+ let string: String
+ switch state.currency {
+ case .stars:
+ string = environment.strings.Gift_Offer_PriceDescriptionStars(giftTitle).string
+ case .ton:
+ string = environment.strings.Gift_Offer_PriceDescriptionTon(giftTitle).string
+ }
+ let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural))
+ amountFooter = AnyComponent(MultilineTextComponent(
+ text: .plain(amountInfoString),
+ maximumNumberOfLines: 0
+ ))
default:
amountFooter = nil
}
@@ -583,6 +627,7 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0)))
}
+ var durationFrame = CGRect()
if case .starGiftResell = component.mode {
contentSize.height += 24.0
@@ -595,12 +640,14 @@ private final class SheetContent: CombinedComponent {
let onlyTonSection = onlyTonSection.update(
component: ListSectionComponent(
theme: theme,
+ style: .glass,
header: nil,
footer: onlyTonFooter,
items: [AnyComponentWithIdentity(
id: "switch",
component: AnyComponent(ListActionItemComponent(
theme: theme,
+ style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -686,12 +733,14 @@ private final class SheetContent: CombinedComponent {
let timestampSection = timestampSection.update(
component: ListSectionComponent(
theme: theme,
+ style: .glass,
header: nil,
footer: timestampFooter,
items: [AnyComponentWithIdentity(
id: "timestamp",
component: AnyComponent(ListActionItemComponent(
theme: theme,
+ style: .glass,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@@ -743,6 +792,80 @@ private final class SheetContent: CombinedComponent {
.cornerRadius(10.0)
)
contentSize.height += timestampSection.size.height
+ } else if case let .starGiftOffer(peer, _, _) = component.mode {
+ contentSize.height += 24.0
+
+ let peerName = peer.compactDisplayTitle
+
+ let footerString: String
+ switch state.currency {
+ case .stars:
+ footerString = environment.strings.Gift_Offer_DurationDescriptionStars(peerName).string
+ case .ton:
+ footerString = environment.strings.Gift_Offer_DurationDescriptionTon(peerName).string
+ }
+
+ let periodFooterString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(footerString, attributes: amountMarkdownAttributes, textAlignment: .natural))
+ let periodFooter = AnyComponent(MultilineTextComponent(
+ text: .plain(periodFooterString),
+ maximumNumberOfLines: 0
+ ))
+
+ let hours = state.duration / 3600
+ let durationString = environment.strings.Gift_Offer_Duration_Hours(hours)
+
+ let periodSection = periodSection.update(
+ component: ListSectionComponent(
+ theme: theme,
+ style: .glass,
+ header: nil,
+ footer: periodFooter,
+ items: [AnyComponentWithIdentity(
+ id: "timestamp",
+ component: AnyComponent(ListActionItemComponent(
+ theme: theme,
+ style: .glass,
+ title: AnyComponent(VStack([
+ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: environment.strings.Gift_Offer_Duration,
+ font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
+ textColor: environment.theme.list.itemPrimaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ ))),
+ ], alignment: .left, spacing: 2.0)),
+ icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
+ text: .plain(NSAttributedString(
+ string: durationString,
+ font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
+ textColor: environment.theme.list.itemSecondaryTextColor
+ )),
+ maximumNumberOfLines: 1
+ )))),
+ accessory: .expandArrows,
+ action: { [weak state] _ in
+ guard let state else {
+ return
+ }
+ state.isPickingDuration = true
+ state.updated(transition: .easeInOut(duration: 0.25))
+ }
+ ))
+ )]
+ ),
+ environment: {},
+ availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
+ transition: context.transition
+ )
+ context.add(periodSection
+ .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + periodSection.size.height / 2.0))
+ .clipsToBounds(true)
+ .cornerRadius(10.0)
+ )
+ durationFrame = CGRect(origin: CGPoint(x: context.availableSize.width / 2.0 - periodSection.size.width / 2.0, y: contentSize.height), size: periodSection.size)
+
+ contentSize.height += periodSection.size.height
}
contentSize.height += 32.0
@@ -789,6 +912,22 @@ private final class SheetContent: CombinedComponent {
case .admin:
buttonString = environment.strings.Chat_PostSuggestion_Suggest_UpdateButton
}
+ } else if case .starGiftOffer = component.mode {
+ if let amount = state.amount {
+ let currencySymbol: String
+ let currencyAmount: String
+ switch state.currency {
+ case .stars:
+ currencySymbol = "#"
+ currencyAmount = presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator)
+ case .ton:
+ currencySymbol = "$"
+ currencyAmount = formatTonAmountText(amount.value, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil)
+ }
+ buttonString = "\(environment.strings.Gift_Offer_Offer) \(currencySymbol) \(currencyAmount)"
+ } else {
+ buttonString = environment.strings.Gift_Offer_Offer
+ }
} else if let amount = state.amount {
buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else {
@@ -913,8 +1052,9 @@ private final class SheetContent: CombinedComponent {
}
}
}
-
completion(CurrencyAmount(amount: amount, currency: state.currency), state.timestamp)
+ case let .starGiftOffer(_, _, completion):
+ completion(CurrencyAmount(amount: amount, currency: state.currency), state.duration)
}
controller.dismissAnimated()
@@ -937,6 +1077,59 @@ private final class SheetContent: CombinedComponent {
contentSize.height += buttonInsets.bottom
}
+
+ if state.isPickingDuration {
+ let sourceFrame = CGRect(origin: CGPoint(x: durationFrame.maxX - 52.0, y: durationFrame.minY + 120.0), size: CGSize(width: 52.0, height: 52.0))
+ let durationPicker = durationPicker.update(
+ component: MenuComponent(
+ theme: theme,
+ sourceFrame: sourceFrame,
+ component: AnyComponent(DurationMenuComponent(
+ theme: theme,
+ strings: environment.strings,
+ value: state.duration,
+ valueUpdated: { [weak state] value in
+ guard let state else {
+ return
+ }
+ state.isPickingDuration = false
+ state.duration = value
+
+ state.updated(transition: .easeInOut(duration: 0.25))
+ }
+ )),
+ dismiss: { [weak state] in
+ guard let state else {
+ return
+ }
+ state.isPickingDuration = false
+ state.updated(transition: .easeInOut(duration: 0.25))
+ }
+ ),
+ availableSize: contentSize,
+ transition: context.transition
+ )
+ context.add(durationPicker
+ .position(CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0))
+ .appear(ComponentTransition.Appear({ _, view, transition in
+ if !transition.animation.isImmediate {
+ if let view = view as? MenuComponent.View {
+ view.animateIn()
+ }
+ }
+ }))
+ .disappear(ComponentTransition.Disappear({ view, transition, completion in
+ if !transition.animation.isImmediate {
+ if let view = view as? MenuComponent.View {
+ view.animateOut(completion: completion)
+ }
+ } else {
+ completion()
+ }
+ }))
+ )
+ }
+
return contentSize
}
@@ -949,9 +1142,11 @@ private final class SheetContent: CombinedComponent {
fileprivate var component: SheetContent
+ fileprivate var forceUpdateAmount = false
fileprivate var amount: StarsAmount?
fileprivate var currency: CurrencyAmount.Currency = .stars
fileprivate var timestamp: Int32?
+ fileprivate var duration: Int32 = 172800
fileprivate var starsBalance: StarsAmount?
private var starsStateDisposable: Disposable?
@@ -962,6 +1157,8 @@ private final class SheetContent: CombinedComponent {
var cachedTonImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
+ var isPickingDuration = false
+
init(component: SheetContent) {
self.context = component.context
self.mode = component.mode
@@ -986,6 +1183,8 @@ private final class SheetContent: CombinedComponent {
currency = initialValue.currency
amount = initialValue.amount
self.timestamp = initialTimestamp
+ case .starGiftOffer:
+ amount = nil
}
self.currency = currency
@@ -1043,7 +1242,7 @@ private final class SheetContent: CombinedComponent {
self.currency = .stars
}
} else {
- let _ = (context.engine.payments.cachedStarGifts()
+ let _ = (self.context.engine.payments.cachedStarGifts()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] gifts in
@@ -1061,6 +1260,7 @@ private final class SheetContent: CombinedComponent {
}
if case let .generic(genericGift) = matchingGift, let minResaleStars = genericGift.availability?.minResaleStars {
self.amount = StarsAmount(value: max(minResaleStars, resaleConfiguration.starGiftResaleMinStarsAmount), nanos: 0)
+ self.forceUpdateAmount = true
self.updated()
}
})
@@ -1178,6 +1378,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case starGiftResell(StarGift.UniqueGift, Bool, completion: (CurrencyAmount) -> Void)
case paidMessages(current: Int64, minValue: Int64, fractionAfterCommission: Int, kind: StarsWithdrawalScreenSubject.PaidMessageKind, completion: (Int64) -> Void)
case suggestedPost(mode: SuggestedPostMode, price: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void)
+ case starGiftOffer(peer: EnginePeer, gift: StarGift.UniqueGift, completion: (CurrencyAmount, Int32) -> Void)
}
private let context: AccountContext
@@ -1213,6 +1414,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View {
Queue.mainQueue().after(0.01) {
+ view.resetValue()
view.activateInput()
view.selectAll()
}
@@ -1247,6 +1449,14 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
case .ton:
break
}
+ } else if case .starGiftOffer = self.mode {
+ switch currency {
+ case .stars:
+ text = presentationData.strings.Gift_Offer_GiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string
+ case .ton:
+ let amountString = formatTonAmountText(minAmount, dateTimeFormat: presentationData.dateTimeFormat) + " TON"
+ text = presentationData.strings.Gift_Offer_GiftMinAmountToast_Text(amountString).string
+ }
}
let resultController = UndoOverlayController(
@@ -1606,6 +1816,7 @@ public final class AmountFieldComponent: Component {
return
}
self.textField.text = "\(value)"
+ self.placeholderView.view?.isHidden = self.textField.text?.isEmpty ?? false
}
func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
@@ -1625,10 +1836,10 @@ public final class AmountFieldComponent: Component {
text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))"
}
self.textField.text = text
- self.didSetValueOnce = true
} else {
self.textField.text = ""
}
+ self.didSetValueOnce = true
}
self.textField.font = Font.regular(17.0)
@@ -2119,3 +2330,417 @@ private final class CurrencyTabItemComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
+
+
+private final class MenuComponent: Component {
+ let theme: PresentationTheme
+ let sourceFrame: CGRect
+ let component: AnyComponent
+ let dismiss: () -> Void
+
+ init(
+ theme: PresentationTheme,
+ sourceFrame: CGRect,
+ component: AnyComponent,
+ dismiss: @escaping () -> Void
+ ) {
+ self.theme = theme
+ self.sourceFrame = sourceFrame
+ self.component = component
+ self.dismiss = dismiss
+ }
+
+ public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool {
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.sourceFrame != rhs.sourceFrame {
+ return false
+ }
+ if lhs.component != rhs.component {
+ return false
+ }
+ return true
+ }
+
+ public final class View: UIView {
+ private let buttonView: UIButton
+ private let containerView: GlassBackgroundContainerView
+ private let backgroundView: GlassBackgroundView
+ private var componentView: ComponentView?
+
+ private var component: MenuComponent?
+
+ public override init(frame: CGRect) {
+ self.buttonView = UIButton()
+ self.containerView = GlassBackgroundContainerView()
+ self.backgroundView = GlassBackgroundView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.buttonView)
+ self.addSubview(self.containerView)
+ self.containerView.contentView.addSubview(self.backgroundView)
+
+ self.buttonView.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
+ }
+
+ public required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func tapped() {
+ if let component = self.component {
+ component.dismiss()
+ }
+ }
+
+ func animateIn() {
+ guard let component = self.component else {
+ return
+ }
+ let transition = ComponentTransition.spring(duration: 0.3)
+ transition.animatePosition(view: self.backgroundView, from: component.sourceFrame.center, to: self.backgroundView.center)
+ transition.animateScale(view: self.backgroundView, from: 0.2, to: 1.0)
+ self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
+ }
+
+ public func animateOut(completion: (() -> Void)? = nil) {
+ guard let component = self.component else {
+ return
+ }
+
+ let transition = ComponentTransition.spring(duration: 0.3)
+ transition.setPosition(view: self.backgroundView, position: component.sourceFrame.center)
+ transition.setScale(view: self.backgroundView, scale: 0.2)
+ self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
+ completion?()
+ })
+ }
+
+ public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ if !self.backgroundView.frame.contains(point) && self.buttonView.frame.contains(point) {
+ return self.buttonView
+ }
+ return super.hitTest(point, with: event)
+ }
+
+ func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+
+ var componentView: ComponentView
+ var componentTransition = transition
+ if let current = self.componentView {
+ componentView = current
+ } else {
+ componentTransition = .immediate
+ componentView = ComponentView()
+ self.componentView = componentView
+ }
+
+ let componentSize = componentView.update(
+ transition: componentTransition,
+ component: component.component,
+ environment: {},
+ containerSize: availableSize
+ )
+ let backgroundFrame = CGRect(origin: CGPoint(x: component.sourceFrame.maxX - componentSize.width, y: component.sourceFrame.minY - componentSize.height - 20.0), size: componentSize)
+ if let view = componentView.view {
+ if view.superview == nil {
+ self.backgroundView.contentView.addSubview(view)
+ }
+ componentTransition.setFrame(view: view, frame: CGRect(origin: .zero, size: componentSize))
+ }
+
+ self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition)
+ self.backgroundView.frame = backgroundFrame
+
+ self.containerView.frame = CGRect(origin: .zero, size: availableSize)
+ self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition)
+
+ self.buttonView.frame = CGRect(origin: .zero, size: availableSize)
+
+ return availableSize
+ }
+ }
+
+ public func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+private final class MenuButtonComponent: Component {
+ let theme: PresentationTheme
+ let text: String
+ let isSelected: Bool
+ let width: CGFloat?
+ let action: () -> Void
+
+ init(
+ theme: PresentationTheme,
+ text: String,
+ isSelected: Bool,
+ width: CGFloat?,
+ action: @escaping () -> Void
+ ) {
+ self.theme = theme
+ self.text = text
+ self.isSelected = isSelected
+ self.width = width
+ self.action = action
+ }
+
+ static func ==(lhs: MenuButtonComponent, rhs: MenuButtonComponent) -> Bool {
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.text != rhs.text {
+ return false
+ }
+ if lhs.isSelected != rhs.isSelected {
+ return false
+ }
+ if lhs.width != rhs.width {
+ return false
+ }
+ return true
+ }
+
+ final class View: UIView {
+ private var component: MenuButtonComponent?
+ private weak var componentState: EmptyComponentState?
+
+ private let selectionLayer = SimpleLayer()
+ private let title = ComponentView()
+ private let icon = ComponentView()
+ private let button = HighlightTrackingButton()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ self.layer.addSublayer(self.selectionLayer)
+ self.selectionLayer.masksToBounds = true
+ self.selectionLayer.opacity = 0.0
+
+ self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
+
+ self.button.highligthedChanged = { [weak self] highlighted in
+ if let self {
+ if highlighted {
+ self.selectionLayer.opacity = 1.0
+ self.selectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
+ } else {
+ self.selectionLayer.opacity = 0.0
+ self.selectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
+ }
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func buttonPressed() {
+ if let component = self.component {
+ component.action()
+ }
+ }
+
+ func update(component: MenuButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+ self.componentState = state
+
+ let leftInset: CGFloat = 60.0
+ let rightInset: CGFloat = 40.0
+
+ let titleSize = self.title.update(
+ transition: transition,
+ component: AnyComponent(
+ Text(text: component.text, font: Font.regular(17.0), color: component.theme.contextMenu.primaryColor)
+ ),
+ environment: {},
+ containerSize: availableSize
+ )
+ let titleFrame = CGRect(origin: CGPoint(x: 60.0, y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
+ if let titleView = self.title.view {
+ if titleView.superview == nil {
+ self.addSubview(titleView)
+ }
+ titleView.frame = titleFrame
+ }
+
+ let size = CGSize(width: component.width ?? (leftInset + rightInset + titleSize.width), height: availableSize.height)
+
+ if component.isSelected {
+ let iconSize = self.icon.update(
+ transition: .immediate,
+ component: AnyComponent(
+ BundleIconComponent(
+ name: "Media Gallery/Check",
+ tintColor: component.theme.contextMenu.primaryColor
+ )
+ ),
+ environment: {},
+ containerSize: CGSize(width: 44.0, height: 44.0)
+ )
+ let iconFrame = CGRect(origin: CGPoint(x: 25.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
+ if let iconView = self.icon.view {
+ if iconView.superview == nil {
+ self.addSubview(iconView)
+ }
+ iconView.frame = iconFrame
+ }
+ }
+
+ self.selectionLayer.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor.withMultipliedAlpha(0.5).cgColor
+ transition.setFrame(layer: self.selectionLayer, frame: CGRect(origin: .zero, size: size).insetBy(dx: 10.0, dy: 0.0))
+ self.selectionLayer.cornerRadius = size.height / 2.0
+
+ if self.button.superview == nil {
+ self.addSubview(self.button)
+ }
+ self.button.frame = CGRect(origin: .zero, size: size)
+
+ return size
+ }
+ }
+
+ func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
+
+private final class DurationMenuComponent: Component {
+ let theme: PresentationTheme
+ let strings: PresentationStrings
+ let value: Int32
+ let valueUpdated: (Int32) -> Void
+
+ init(
+ theme: PresentationTheme,
+ strings: PresentationStrings,
+ value: Int32,
+ valueUpdated: @escaping (Int32) -> Void
+ ) {
+ self.theme = theme
+ self.strings = strings
+ self.value = value
+ self.valueUpdated = valueUpdated
+ }
+
+ public static func ==(lhs: DurationMenuComponent, rhs: DurationMenuComponent) -> Bool {
+ if lhs.theme !== rhs.theme {
+ return false
+ }
+ if lhs.strings !== rhs.strings {
+ return false
+ }
+ if lhs.value != rhs.value {
+ return false
+ }
+ return true
+ }
+
+ public final class View: UIView {
+ private let backgroundView: GlassBackgroundView
+ private var itemViews: [Int32: ComponentView] = [:]
+
+ private var component: DurationMenuComponent?
+
+ private let values: [Int32] = [
+ 21600, 43200, 86400, 129600, 172800, 259200
+ ]
+
+ private var width: CGFloat?
+
+ public override init(frame: CGRect) {
+ self.backgroundView = GlassBackgroundView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.backgroundView)
+ }
+
+ public required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func update(component: DurationMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ self.component = component
+
+ let itemHeight: CGFloat = 40.0
+
+ var maxWidth: CGFloat = 0.0
+ var originY: CGFloat = 12.0
+ for value in self.values {
+ let itemView: ComponentView
+ if let current = self.itemViews[value] {
+ itemView = current
+ } else {
+ itemView = ComponentView()
+ self.itemViews[value] = itemView
+ }
+
+ let hours = value / 3600
+ let repeatString: String = component.strings.Gift_Offer_Duration_Hours(Int32(hours))
+
+ let itemSize = itemView.update(
+ transition: transition,
+ component: AnyComponent(
+ MenuButtonComponent(
+ theme: component.theme,
+ text: repeatString,
+ isSelected: component.value == value,
+ width: self.width,
+ action: { [weak self] in
+ guard let self else {
+ return
+ }
+ self.component?.valueUpdated(value)
+ }
+ )
+ ),
+ environment: {},
+ containerSize: CGSize(width: availableSize.width, height: itemHeight)
+ )
+ maxWidth = max(maxWidth, itemSize.width)
+ let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: originY), size: itemSize)
+ if let itemView = itemView.view {
+ if itemView.superview == nil {
+ self.addSubview(itemView)
+ }
+ transition.setFrame(view: itemView, frame: itemFrame)
+ }
+ originY += 40.0
+ }
+
+ let size = CGSize(width: maxWidth, height: originY + 8.0)
+
+ if self.width == nil {
+ self.width = maxWidth
+ Queue.mainQueue().justDispatch {
+ state.updated()
+ }
+ }
+
+ return size
+ }
+ }
+
+ public func makeView() -> View {
+ return View(frame: CGRect())
+ }
+
+ public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize {
+ return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
+ }
+}
diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD
index dc32b21d4f..096451d9b2 100644
--- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD
+++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD
@@ -29,6 +29,7 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/TelegramUI/Components/SliderComponent",
+ "//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/CheckNode",
"//submodules/Markdown",
"//submodules/ContextUI",
diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift
index 4ceb81cb3c..bac4438def 100644
--- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift
+++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift
@@ -23,6 +23,7 @@ import TelegramStringFormatting
import GalleryData
import AnimatedTextComponent
import TelegramUIPreferences
+import SegmentControlComponent
final class DataUsageScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift
index 6567c1d2a5..3e4d303edd 100644
--- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift
+++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift
@@ -464,6 +464,7 @@ public final class PeerListItemComponent: Component {
private var isExtractedToContextMenu: Bool = false
public var customUpdateIsHighlighted: ((Bool) -> Void)?
+ public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift
index 3231872c39..f74712273d 100644
--- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift
+++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift
@@ -576,10 +576,12 @@ final class StoryContentLiveChatComponent: Component {
if !self.isChatExpanded {
var hasNewMessages = false
for message in state.messages {
- if message.isIncoming && !previousMessagesState.messages.contains(where: { $0.id == message.id }) {
- hasNewMessages = true
+ if message.isIncoming {
+ if !previousMessagesState.messages.contains(where: { $0.id == message.id }) {
+ hasNewMessages = true
+ }
- if message.isIncoming, let paidStars = message.paidStars, let author = message.author {
+ if let paidStars = message.paidStars, let author = message.author {
self.reactionStreamView?.add(peer: author, count: Int(paidStars))
}
}
@@ -589,6 +591,16 @@ final class StoryContentLiveChatComponent: Component {
}
}
+ for message in state.messages {
+ if message.isIncoming {
+ if !previousMessagesState.messages.contains(where: { $0.id == message.id }) {
+ if let paidStars = message.paidStars, let author = message.author {
+ self.reactionStreamView?.add(peer: author, count: Int(paidStars))
+ }
+ }
+ }
+ }
+
if state.pendingMyStars > previousMessagesState.pendingMyStars, let message = state.messages.first(where: { $0.paidStars != nil && !$0.isIncoming }), let peer = message.author {
self.reactionStreamView?.add(peer: peer, count: Int(state.pendingMyStars - previousMessagesState.pendingMyStars))
}
diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD
index 2dc21d7382..bf1656c7b3 100644
--- a/submodules/TelegramUI/Components/TabBarComponent/BUILD
+++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD
@@ -20,6 +20,8 @@ swift_library(
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/TextBadgeComponent",
"//submodules/UIKitRuntimeUtils",
+ "//submodules/TelegramUI/Components/LiquidLens",
+ "//submodules/AppBundle",
],
visibility = [
"//visibility:public",
diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift
index 6f9276d4d6..585e15ed8c 100644
--- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift
+++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift
@@ -10,6 +10,97 @@ import LottieComponent
import UIKitRuntimeUtils
import BundleIconComponent
import TextBadgeComponent
+import LiquidLens
+import AppBundle
+
+private final class TabSelectionRecognizer: UIGestureRecognizer {
+ private var initialLocation: CGPoint?
+ private var currentLocation: CGPoint?
+
+ override init(target: Any?, action: Selector?) {
+ super.init(target: target, action: action)
+
+ self.delaysTouchesBegan = false
+ self.delaysTouchesEnded = false
+ }
+
+ override func reset() {
+ super.reset()
+
+ self.initialLocation = nil
+ }
+
+ override func touchesBegan(_ touches: Set, with event: UIEvent) {
+ super.touchesBegan(touches, with: event)
+
+ if self.initialLocation == nil {
+ self.initialLocation = touches.first?.location(in: self.view)
+ }
+ self.currentLocation = self.initialLocation
+
+ self.state = .began
+ }
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent) {
+ super.touchesEnded(touches, with: event)
+
+ self.state = .ended
+ }
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent) {
+ super.touchesCancelled(touches, with: event)
+
+ self.state = .cancelled
+ }
+
+ override func touchesMoved(_ touches: Set, with event: UIEvent) {
+ super.touchesMoved(touches, with: event)
+
+ self.currentLocation = touches.first?.location(in: self.view)
+
+ self.state = .changed
+ }
+
+ func translation(in: UIView?) -> CGPoint {
+ if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
+ return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
+ }
+ return CGPoint()
+ }
+}
+
+public final class TabBarSearchView: UIView {
+ private let backgroundView: GlassBackgroundView
+ private let iconView: GlassBackgroundView.ContentImageView
+
+ override public init(frame: CGRect) {
+ self.backgroundView = GlassBackgroundView()
+ self.iconView = GlassBackgroundView.ContentImageView()
+
+ super.init(frame: frame)
+
+ self.addSubview(self.backgroundView)
+ self.backgroundView.contentView.addSubview(self.iconView)
+ }
+
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) {
+ transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
+ self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition)
+
+ if self.iconView.image == nil {
+ self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
+ }
+ self.iconView.tintColor = iconColor
+
+ if let image = self.iconView.image {
+ transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size))
+ }
+ }
+}
public final class TabBarComponent: Component {
public final class Item: Equatable {
@@ -75,63 +166,27 @@ public final class TabBarComponent: Component {
}
public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate {
- private let backgroundView: GlassBackgroundView
- private let selectionView: GlassBackgroundView.ContentImageView
+ private let liquidLensView: LiquidLensView
private let contextGestureContainerView: ContextControllerSourceView
- private let nativeTabBar: UITabBar?
private var itemViews: [AnyHashable: ComponentView