From 13831f909f3f9631510122f8727c5c84a6d4276e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 1 Apr 2020 21:56:51 +0400 Subject: [PATCH 1/2] Wallet-related changes --- Wallet.makefile | 58 ++----------- Wallet/README.md | 50 ++--------- extract_wallet_source.py | 26 ++---- .../Sources/WalletReceiveScreen.swift | 86 ++++++++++++------- wallet_env.sh | 12 +++ 5 files changed, 85 insertions(+), 147 deletions(-) diff --git a/Wallet.makefile b/Wallet.makefile index b2d9c44ab0..d58b1d5f1b 100644 --- a/Wallet.makefile +++ b/Wallet.makefile @@ -1,28 +1,7 @@ -include Utils.makefile - APP_VERSION="1.0" CORE_COUNT=$(shell sysctl -n hw.logicalcpu) CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1) -WALLET_BUCK_OPTIONS=\ - --config custom.appVersion="1.0" \ - --config custom.developmentCodeSignIdentity="${DEVELOPMENT_CODE_SIGN_IDENTITY}" \ - --config custom.distributionCodeSignIdentity="${DISTRIBUTION_CODE_SIGN_IDENTITY}" \ - --config custom.developmentTeam="${DEVELOPMENT_TEAM}" \ - --config custom.baseApplicationBundleId="${WALLET_BUNDLE_ID}" \ - --config custom.buildNumber="${BUILD_NUMBER}" \ - --config custom.entitlementsApp="${WALLET_ENTITLEMENTS_APP}" \ - --config custom.developmentProvisioningProfileApp="${WALLET_DEVELOPMENT_PROVISIONING_PROFILE_APP}" \ - --config custom.distributionProvisioningProfileApp="${WALLET_DISTRIBUTION_PROVISIONING_PROFILE_APP}" \ - --config custom.apiId="${API_ID}" \ - --config custom.apiHash="${API_HASH}" \ - --config custom.appCenterId="0" \ - --config custom.isInternalBuild="${IS_INTERNAL_BUILD}" \ - --config custom.isAppStoreBuild="${IS_APPSTORE_BUILD}" \ - --config custom.appStoreId="${APPSTORE_ID}" \ - --config custom.appSpecificUrlScheme="${APP_SPECIFIC_URL_SCHEME}" \ - --config buildfile.name=BUCK - BAZEL=$(shell which bazel) ifneq ($(BAZEL_CACHE_DIR),) @@ -42,37 +21,10 @@ BAZEL_OPT_FLAGS=\ --swiftcopt=-whole-module-optimization \ --swiftcopt='-num-threads' --swiftcopt='16' \ -wallet_deps: check_env - $(BUCK) query "deps(//Wallet:AppPackage)" --output-attribute buck.type \ - ${WALLET_BUCK_OPTIONS} ${BUCK_RELEASE_OPTIONS} +kill_xcode: + killall Xcode || true -wallet_project: check_env kill_xcode - $(BUCK) project //Wallet:workspace --config custom.mode=project ${WALLET_BUCK_OPTIONS} ${BUCK_DEBUG_OPTIONS} - open Wallet/WalletWorkspace.xcworkspace - -build_wallet: check_env - $(BUCK) build \ - //Wallet:AppPackage#iphoneos-arm64,iphoneos-armv7 \ - //Wallet:Wallet#dwarf-and-dsym,iphoneos-arm64,iphoneos-armv7 \ - //submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#dwarf-and-dsym,shared,iphoneos-arm64,iphoneos-armv7 \ - //submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared,iphoneos-arm64,iphoneos-armv7 \ - //submodules/AsyncDisplayKit:AsyncDisplayKit#dwarf-and-dsym,shared,iphoneos-arm64,iphoneos-armv7 \ - //submodules/AsyncDisplayKit:AsyncDisplayKit#shared,iphoneos-arm64,iphoneos-armv7 \ - //submodules/Display:Display#dwarf-and-dsym,shared,iphoneos-arm64,iphoneos-armv7 \ - //submodules/Display:Display#shared,iphoneos-arm64,iphoneos-armv7 \ - ${WALLET_BUCK_OPTIONS} ${BUCK_RELEASE_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_CACHE_OPTIONS} - -wallet_package: - PACKAGE_DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ - PACKAGE_CODE_SIGN_IDENTITY="${DISTRIBUTION_CODE_SIGN_IDENTITY}" \ - PACKAGE_PROVISIONING_PROFILE_APP="${WALLET_DISTRIBUTION_PROVISIONING_PROFILE_APP}" \ - PACKAGE_ENTITLEMENTS_APP="Wallet/${WALLET_ENTITLEMENTS_APP}" \ - PACKAGE_BUNDLE_ID="${WALLET_BUNDLE_ID}" \ - sh package_app.sh iphoneos-arm64,iphoneos-armv7 $(BUCK) "wallet" $(WALLET_BUCK_OPTIONS) ${BUCK_RELEASE_OPTIONS} - -wallet_app: build_wallet wallet_package - -bazel_wallet_debug_arm64: +wallet_app_debug_arm64: WALLET_APP_VERSION="${APP_VERSION}" \ build-system/prepare-build.sh Wallet distribution "${BAZEL}" build Wallet/Wallet ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_DEBUG_FLAGS} \ @@ -81,7 +33,7 @@ bazel_wallet_debug_arm64: --watchos_cpus=armv7k,arm64_32 \ --verbose_failures -bazel_wallet: +wallet_app: WALLET_APP_VERSION="${APP_VERSION}" \ build-system/prepare-build.sh Wallet distribution "${BAZEL}" build Wallet/Wallet ${BAZEL_CACHE_FLAGS} ${BAZEL_COMMON_FLAGS} ${BAZEL_OPT_FLAGS} \ @@ -95,7 +47,7 @@ bazel_wallet_prepare_development_build: BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ build-system/prepare-build.sh Wallet development -bazel_wallet_project: kill_xcode bazel_wallet_prepare_development_build +wallet_project: kill_xcode bazel_wallet_prepare_development_build WALLET_APP_VERSION="${APP_VERSION}" \ BAZEL_CACHE_DIR="${BAZEL_CACHE_DIR}" \ build-system/generate-xcode-project.sh Wallet diff --git a/Wallet/README.md b/Wallet/README.md index fc0c50909a..67ecc83239 100644 --- a/Wallet/README.md +++ b/Wallet/README.md @@ -2,62 +2,26 @@ This is the source code and build instructions for a TON Testnet Wallet implementation for iOS. -1. Install Xcode 11.1 +1. Install Xcode 11.4 ``` -https://apps.apple.com/ae/app/xcode/id497799835?mt=12 +https://apps.apple.com/app/xcode/id497799835 ``` Make sure to launch Xcode at least once and set up command-line tools paths (Xcode — Preferences — Locations — Command Line Tools) -2. Install Homebrew - -``` -/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" -``` -3. Install the required tools - -``` -brew tap AdoptOpenJDK/openjdk -brew cask install adoptopenjdk8 -brew install cmake ant -``` - -4. Build Buck - -``` -mkdir -p $HOME/buck_source -cd tools/buck-build -sh ./prepare_buck_source.sh $HOME/buck_source -``` - -5. Now you can build Wallet application (IPA) +2. Build the app (IPA) Note: It is recommended to use an artifact cache to optimize build speed. Prepend any of the following commands with ``` -BUCK_DIR_CACHE="path/to/existing/directory" +BAZEL_CACHE_DIR="path/to/existing/directory" ``` ``` -BUCK="$HOME/buck_source/buck/buck-out/gen/programs/buck.pex" \ - BUILD_NUMBER=30 \ - DISTRIBUTION_CODE_SIGN_IDENTITY="iPhone Distribution: XXXXXXX (XXXXXXXXXX)" \ - DEVELOPMENT_TEAM="XXXXXXXXXX" WALLET_BUNDLE_ID="wallet.bundle.id" \ - WALLET_DISTRIBUTION_PROVISIONING_PROFILE_APP="wallet distribution provisioning profile name" \ - CODESIGNING_SOURCE_DATA_PATH="$HOME/wallet_codesigning" \ - sh Wallet/example_wallet_env.sh make -f Wallet.makefile wallet_app +sh wallet_env.sh make wallet_app ``` -6. If needed, generate Xcode project +3. If needed, generate Xcode project ``` -BUCK="$HOME/buck_source/buck/buck-out/gen/programs/buck.pex" \ - BUILD_NUMBER=30 \ - DEVELOPMENT_CODE_SIGN_IDENTITY="iPhone Developer: XXXXXXX (XXXXXXXXXX)" \ - DISTRIBUTION_CODE_SIGN_IDENTITY="iPhone Distribution: XXXXXXX (XXXXXXXXXX)" \ - DEVELOPMENT_TEAM="XXXXXXXXXX" WALLET_BUNDLE_ID="wallet.bundle.id" \ - WALLET_DEVELOPMENT_PROVISIONING_PROFILE_APP="wallet development provisioning profile name" \ - WALLET_DISTRIBUTION_PROVISIONING_PROFILE_APP="wallet distribution provisioning profile name" \ - CODESIGNING_SOURCE_DATA_PATH="$HOME/wallet_codesigning" \ - sh Wallet/example_wallet_env.sh make -f Wallet.makefile wallet_project +sh wallet_env.sh make wallet_project ``` - diff --git a/extract_wallet_source.py b/extract_wallet_source.py index 025e79bc3f..b7d22657ba 100644 --- a/extract_wallet_source.py +++ b/extract_wallet_source.py @@ -39,22 +39,6 @@ def clean_copy_files(dir, destination_dir): continue mkdir_p(destination_dir + "/" + dir_path) -def clean_files(base_dir, dirs, files): - for file in files: - if file == '.DS_Store': - os.remove(base_dir + '/' + file) - for dir in dirs: - if re.match('.*\\.xcodeproj', dir) or re.match('.*\\.xcworkspace', dir): - shutil.rmtree(base_dir + '/' + dir, ignore_errors=True) - -def clean_dep_files(base_dir, dirs, files): - for file in files: - if re.match('^\\.git$', file) or re.match('^.*/\\.git$', file): - os.remove(base_dir + '/' + file) - for dir in dirs: - if re.match('^\\.git$', dir) or re.match('^.*/\\.git$', dir): - shutil.rmtree(base_dir + '/' + dir, ignore_errors=True) - if len(sys.argv) != 2: print('Usage: extract_wallet_source.py destination') sys.exit(1) @@ -96,13 +80,15 @@ additional_paths = [ "build-system/copy-provisioning-profiles-Wallet.sh", "build-system/prepare-build-variables-Wallet.sh", ".bazelrc", - "Utils.makefile", - "Wallet.makefile", "wallet_env.sh", ] for file_path in additional_paths: if os.path.isdir(file_path): - clean_copy_files(file_path, destination + '/' + file_path) + clean_copy_files(file_path, destination + "/" + file_path) else: - shutil.copy(file_path, destination + '/' + file_path) + shutil.copy(file_path, destination + "/" + file_path) + +shutil.copy("Wallet.makefile", destination + "/" + "Makefile") +shutil.copy("Wallet/README.md", destination + "/" + "README.md") + diff --git a/submodules/WalletUI/Sources/WalletReceiveScreen.swift b/submodules/WalletUI/Sources/WalletReceiveScreen.swift index 8415854ac9..4032cd31fc 100644 --- a/submodules/WalletUI/Sources/WalletReceiveScreen.swift +++ b/submodules/WalletUI/Sources/WalletReceiveScreen.swift @@ -170,7 +170,7 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { self.textNode = ImmediateTextNode() self.textNode.textAlignment = .center - self.textNode.maximumNumberOfLines = 3 + self.textNode.maximumNumberOfLines = 5 self.qrImageNode = TransformImageNode() self.qrImageNode.clipsToBounds = true @@ -287,33 +287,6 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationHeight) - var insets = layout.insets(options: []) - insets.top += navigationHeight - let inset: CGFloat = 22.0 - - let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + 24.0), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let makeImageLayout = self.qrImageNode.asyncLayout() - - let imageSide: CGFloat = 215.0 - let imageSize = CGSize(width: imageSide, height: imageSide) - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - - let _ = imageApply() - - let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: textFrame.maxY + 20.0), size: imageSize) - transition.updateFrame(node: self.qrImageNode, frame: imageFrame) - transition.updateFrame(node: self.qrButtonNode, frame: imageFrame) - - if let qrCodeSize = self.qrCodeSize { - let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) - self.qrIconNode.updateLayout(size: cutoutFrame.size) - transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size)) - transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) - } - if self.urlTextNode.attributedText?.string.isEmpty ?? true { var url = urlForMode(self.mode) if case .receive = self.mode { @@ -335,18 +308,69 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { self.urlTextNode.attributedText = NSAttributedString(string: sliced, font: addressFont, textColor: self.presentationData.theme.list.itemPrimaryTextColor, paragraphAlignment: .justified) } - let addressInset: CGFloat = 12.0 - let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - addressInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + 23.0), size: urlTextSize)) + var insets = layout.insets(options: []) + insets.top += navigationHeight + let inset: CGFloat = 22.0 let buttonSideInset: CGFloat = 16.0 let bottomInset = insets.bottom + 10.0 let buttonWidth = layout.size.width - buttonSideInset * 2.0 let buttonHeight: CGFloat = 50.0 + let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let addressInset: CGFloat = 12.0 + let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - addressInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + var buttonOffset: CGFloat = 0.0 if let _ = self.secondaryButtonNode.attributedTitle(for: .normal) { buttonOffset = -60.0 + } + + let imageSide: CGFloat = 215.0 + let imageSize = CGSize(width: imageSide, height: imageSide) + + let buttonTopEdge = layout.size.height - bottomInset - buttonHeight + buttonOffset + + var topTextSpacing: CGFloat = 24.0 + var imageSpacing: CGFloat = 20.0 + var urlSpacing: CGFloat = 23.0 + + var contentHeight: CGFloat = insets.top + contentHeight += topTextSpacing + textSize.height + contentHeight += imageSpacing + imageSide + contentHeight += urlSpacing + urlTextSize.height + + if contentHeight >= buttonTopEdge - 10.0 { + let factor: CGFloat = 0.5 + topTextSpacing = floor(topTextSpacing * factor * 0.3) + imageSpacing = floor(imageSpacing * factor) + urlSpacing = floor(urlSpacing * factor) + } + + let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + topTextSpacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let makeImageLayout = self.qrImageNode.asyncLayout() + + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) + + let _ = imageApply() + + let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: textFrame.maxY + imageSpacing), size: imageSize) + transition.updateFrame(node: self.qrImageNode, frame: imageFrame) + transition.updateFrame(node: self.qrButtonNode, frame: imageFrame) + + if let qrCodeSize = self.qrCodeSize { + let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) + self.qrIconNode.updateLayout(size: cutoutFrame.size) + transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size)) + transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) + } + + transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + urlSpacing), size: urlTextSize)) + + if let _ = self.secondaryButtonNode.attributedTitle(for: .normal) { self.secondaryButtonNode.frame = CGRect(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight, width: buttonWidth, height: buttonHeight) } diff --git a/wallet_env.sh b/wallet_env.sh index 1ec7b2a210..fad3729ac4 100755 --- a/wallet_env.sh +++ b/wallet_env.sh @@ -50,6 +50,18 @@ if [ "$WALLET_DISTRIBUTION_PROVISIONING_PROFILE_APP" == "" ]; then exit 1 fi +if [ "$CODESIGNING_DATA_PATH" == "" ]; then + echo "Set CODESIGNING_DATA_PATH to the path to a folder containing valid provisioning profiles corresponding to the chosen bundle ID ($WALLET_BUNDLE_ID)" + echo "Example: export CODESIGNING_DATA_PATH=\"\$HOME/wallet-provisioning-profiles\"" + exit 1 +fi + +if [ "$BUILD_NUMBER" == "" ]; then + echo "Set BUILD_NUMBER to a number that will be used as a version string for the build" + echo "Example: export BUILD_NUMBER=100" + exit 1 +fi + export DEVELOPMENT_CODE_SIGN_IDENTITY="$DEVELOPMENT_CODE_SIGN_IDENTITY" export DISTRIBUTION_CODE_SIGN_IDENTITY="$DISTRIBUTION_CODE_SIGN_IDENTITY" export WALLET_DEVELOPMENT_TEAM="$WALLET_DEVELOPMENT_TEAM" From 286ce686e649aa8a9a58050be46d043f915e0ea4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 3 Apr 2020 17:56:18 +0400 Subject: [PATCH 2/2] Updated polls --- Telegram/NotificationService/Serialization.m | 2 +- .../Sources/CreatePollController.swift | 61 +++- .../SyncCore/Sources/TelegramMediaPoll.swift | 33 ++- submodules/TelegramApi/Sources/Api0.swift | 6 +- submodules/TelegramApi/Sources/Api1.swift | 96 +++++-- submodules/TelegramApi/Sources/Api3.swift | 31 +- .../Sources/AccountStateManagementUtils.swift | 4 +- .../Sources/ChannelStatistics.swift | 4 +- .../PendingMessageUploadedContent.swift | 10 +- submodules/TelegramCore/Sources/Polls.swift | 13 +- .../TelegramCore/Sources/Serialization.swift | 2 +- .../Sources/StoreMessage_Telegram.swift | 4 +- .../Sources/TelegramMediaPoll.swift | 4 +- .../Resources/PresentationResourceKey.swift | 2 + .../Resources/PresentationResourcesChat.swift | 6 + .../Chat/Message/Lamp.imageset/Contents.json | 12 + .../Message/Lamp.imageset/ic_lamp (1).pdf | Bin 0 -> 4399 bytes .../TelegramUI/Sources/ChatController.swift | 15 +- .../Sources/ChatControllerInteraction.swift | 5 +- .../ChatInterfaceStateContextMenus.swift | 4 +- .../Sources/ChatMessageBubbleItemNode.swift | 4 +- .../ChatMessagePollBubbleContentNode.swift | 219 ++++++++++++++- .../ChatRecentActionsControllerNode.swift | 1 + .../Sources/OverlayPlayerControllerNode.swift | 1 + .../Sources/PeerInfo/PeerInfoScreen.swift | 1 + .../PeerMediaCollectionController.swift | 1 + .../Sources/PollBubbleTimerNode.swift | 264 ++++++++++++++++++ .../Sources/PollResultsController.swift | 63 ++++- .../Sources/SharedAccountContext.swift | 1 + .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 19 +- 31 files changed, 792 insertions(+), 97 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/ic_lamp (1).pdf create mode 100644 submodules/TelegramUI/Sources/PollBubbleTimerNode.swift diff --git a/Telegram/NotificationService/Serialization.m b/Telegram/NotificationService/Serialization.m index b14ed21e17..a66575d16a 100644 --- a/Telegram/NotificationService/Serialization.m +++ b/Telegram/NotificationService/Serialization.m @@ -3,7 +3,7 @@ @implementation Serialization - (NSUInteger)currentLayer { - return 111; + return 112; } - (id _Nullable)parseMessage:(NSData * _Nullable)data { diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index 8fbcbab1ec..8777a747b9 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -159,8 +159,9 @@ private final class CreatePollControllerArguments { let updateMultipleChoice: (Bool) -> Void let displayMultipleChoiceDisabled: () -> Void let updateQuiz: (Bool) -> Void + let updateSolutionText: (String) -> Void - init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void) { + init(updatePollText: @escaping (String) -> Void, updateOptionText: @escaping (Int, String, Bool) -> Void, moveToNextOption: @escaping (Int) -> Void, moveToPreviousOption: @escaping (Int) -> Void, removeOption: @escaping (Int, Bool) -> Void, optionFocused: @escaping (Int, Bool) -> Void, setItemIdWithRevealedOptions: @escaping (Int?, Int?) -> Void, toggleOptionSelected: @escaping (Int) -> Void, updateAnonymous: @escaping (Bool) -> Void, updateMultipleChoice: @escaping (Bool) -> Void, displayMultipleChoiceDisabled: @escaping () -> Void, updateQuiz: @escaping (Bool) -> Void, updateSolutionText: @escaping (String) -> Void) { self.updatePollText = updatePollText self.updateOptionText = updateOptionText self.moveToNextOption = moveToNextOption @@ -173,6 +174,7 @@ private final class CreatePollControllerArguments { self.updateMultipleChoice = updateMultipleChoice self.displayMultipleChoiceDisabled = displayMultipleChoiceDisabled self.updateQuiz = updateQuiz + self.updateSolutionText = updateSolutionText } } @@ -180,6 +182,7 @@ private enum CreatePollSection: Int32 { case text case options case settings + case quizSolution } private enum CreatePollEntryId: Hashable { @@ -192,6 +195,9 @@ private enum CreatePollEntryId: Hashable { case multipleChoice case quiz case quizInfo + case quizSolutionHeader + case quizSolutionText + case quizSolutionInfo } private enum CreatePollEntryTag: Equatable, ItemListItemTag { @@ -218,6 +224,9 @@ private enum CreatePollEntry: ItemListNodeEntry { case multipleChoice(String, Bool, Bool) case quiz(String, Bool) case quizInfo(String) + case quizSolutionHeader(String) + case quizSolutionText(placeholder: String, text: String) + case quizSolutionInfo(String) var section: ItemListSectionId { switch self { @@ -227,6 +236,8 @@ private enum CreatePollEntry: ItemListNodeEntry { return CreatePollSection.options.rawValue case .anonymousVotes, .multipleChoice, .quiz, .quizInfo: return CreatePollSection.settings.rawValue + case .quizSolutionHeader, .quizSolutionText, .quizSolutionInfo: + return CreatePollSection.quizSolution.rawValue } } @@ -262,6 +273,12 @@ private enum CreatePollEntry: ItemListNodeEntry { return .quiz case .quizInfo: return .quizInfo + case .quizSolutionHeader: + return .quizSolutionHeader + case .quizSolutionText: + return .quizSolutionText + case .quizSolutionInfo: + return .quizSolutionInfo } } @@ -285,6 +302,12 @@ private enum CreatePollEntry: ItemListNodeEntry { return 1004 case .quizInfo: return 1005 + case .quizSolutionHeader: + return 1006 + case .quizSolutionText: + return 1007 + case .quizSolutionInfo: + return 1008 } } @@ -352,6 +375,14 @@ private enum CreatePollEntry: ItemListNodeEntry { }) case let .quizInfo(text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .quizSolutionHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .quizSolutionText(placeholder, text): + return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 400, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in + arguments.updateSolutionText(text) + }) + case let .quizSolutionInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } @@ -371,6 +402,7 @@ private struct CreatePollControllerState: Equatable { var isAnonymous: Bool = true var isMultipleChoice: Bool = false var isQuiz: Bool = false + var solutionText: String = "" } private func createPollControllerEntries(presentationData: PresentationData, peer: Peer, state: CreatePollControllerState, limitsConfiguration: LimitsConfiguration, defaultIsQuiz: Bool?) -> [CreatePollEntry] { @@ -410,14 +442,24 @@ private func createPollControllerEntries(presentationData: PresentationData, pee if canBePublic { entries.append(.anonymousVotes(presentationData.strings.CreatePoll_Anonymous, state.isAnonymous)) } + var isQuiz = false if let defaultIsQuiz = defaultIsQuiz { if !defaultIsQuiz { entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) + } else { + isQuiz = true } } else { entries.append(.multipleChoice(presentationData.strings.CreatePoll_MultipleChoice, state.isMultipleChoice && !state.isQuiz, !state.isQuiz)) entries.append(.quiz(presentationData.strings.CreatePoll_Quiz, state.isQuiz)) entries.append(.quizInfo(presentationData.strings.CreatePoll_QuizInfo)) + isQuiz = state.isQuiz + } + + if isQuiz { + entries.append(.quizSolutionHeader("EXPLANATION")) + entries.append(.quizSolutionText(placeholder: "Add a Comment (Optional)", text: state.solutionText)) + entries.append(.quizSolutionInfo("Users will see this comment after choosing a wrong answer, good for educational purposes.")) } return entries @@ -663,6 +705,12 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo if value { displayQuizTooltipImpl?(value) } + }, updateSolutionText: { text in + updateState { state in + var state = state + state.solutionText = text + return state + } }) let previousOptionIds = Atomic<[Int]?>(value: nil) @@ -726,14 +774,23 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo } else { publicity = .public } + var resolvedSolution: String? let kind: TelegramMediaPollKind if state.isQuiz { kind = .quiz + resolvedSolution = state.solutionText.isEmpty ? nil : state.solutionText } else { kind = .poll(multipleAnswers: state.isMultipleChoice) } + + var deadlineTimeout: Int32? + #if DEBUG + deadlineTimeout = 65 + #endif + dismissImpl?() - completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: []), isClosed: false)), replyToMessageId: nil, localGroupingKey: nil)) + + completion(.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: publicity, kind: kind, text: processPollText(state.text), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), isClosed: false, deadlineTimeout: deadlineTimeout)), replyToMessageId: nil, localGroupingKey: nil)) }) let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { diff --git a/submodules/SyncCore/Sources/TelegramMediaPoll.swift b/submodules/SyncCore/Sources/TelegramMediaPoll.swift index 3fb4336fa1..3f8cb15f6d 100644 --- a/submodules/SyncCore/Sources/TelegramMediaPoll.swift +++ b/submodules/SyncCore/Sources/TelegramMediaPoll.swift @@ -53,17 +53,20 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { public let voters: [TelegramMediaPollOptionVoters]? public let totalVoters: Int32? public let recentVoters: [PeerId] + public let solution: String? - public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId]) { + public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: String?) { self.voters = voters self.totalVoters = totalVoters self.recentVoters = recentVoters + self.solution = solution } public init(decoder: PostboxDecoder) { self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v") self.totalVoters = decoder.decodeOptionalInt32ForKey("t") self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init) + self.solution = decoder.decodeOptionalStringForKey("sol") } public func encode(_ encoder: PostboxEncoder) { @@ -78,6 +81,11 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { encoder.encodeNil(forKey: "t") } encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv") + if let solution = self.solution { + encoder.encodeString(solution, forKey: "sol") + } else { + encoder.encodeNil(forKey: "sol") + } } } @@ -130,8 +138,9 @@ public final class TelegramMediaPoll: Media, Equatable { public let correctAnswers: [Data]? public let results: TelegramMediaPollResults public let isClosed: Bool + public let deadlineTimeout: Int32? - public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool) { + public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) { self.pollId = pollId self.publicity = publicity self.kind = kind @@ -140,6 +149,7 @@ public final class TelegramMediaPoll: Media, Equatable { self.correctAnswers = correctAnswers self.results = results self.isClosed = isClosed + self.deadlineTimeout = deadlineTimeout } public init(decoder: PostboxDecoder) { @@ -153,8 +163,9 @@ public final class TelegramMediaPoll: Media, Equatable { self.text = decoder.decodeStringForKey("t", orElse: "") self.options = decoder.decodeObjectArrayWithDecoderForKey("os") self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca") - self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: []) + self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil) self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0 + self.deadlineTimeout = decoder.decodeOptionalInt32ForKey("dt") } public func encode(_ encoder: PostboxEncoder) { @@ -172,6 +183,11 @@ public final class TelegramMediaPoll: Media, Equatable { } encoder.encodeObject(results, forKey: "rs") encoder.encodeInt32(self.isClosed ? 1 : 0, forKey: "ic") + if let deadlineTimeout = self.deadlineTimeout { + encoder.encodeInt32(deadlineTimeout, forKey: "dt") + } else { + encoder.encodeNil(forKey: "dt") + } } public func isEqual(to other: Media) -> Bool { @@ -210,6 +226,9 @@ public final class TelegramMediaPoll: Media, Equatable { if lhs.isClosed != rhs.isClosed { return false } + if lhs.deadlineTimeout != rhs.deadlineTimeout { + return false + } return true } @@ -229,15 +248,15 @@ public final class TelegramMediaPoll: Media, Equatable { } updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier)) - }), totalVoters: results.totalVoters, recentVoters: results.recentVoters) + }), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) } else if let updatedVoters = results.voters { - updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters) + updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) } else { - updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters) + updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) } } else { updatedResults = results } - return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed) + return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout) } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c43e60d1d2..9b60f90d1e 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -11,7 +11,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[461151667] = { return Api.ChatFull.parse_chatFull($0) } dict[-253335766] = { return Api.ChatFull.parse_channelFull($0) } - dict[-932174686] = { return Api.PollResults.parse_pollResults($0) } + dict[-1159937629] = { return Api.PollResults.parse_pollResults($0) } dict[-925415106] = { return Api.ChatParticipant.parse_chatParticipant($0) } dict[-636267638] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) } dict[-489233354] = { return Api.ChatParticipant.parse_chatParticipantAdmin($0) } @@ -297,7 +297,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } dict[1158290442] = { return Api.messages.FoundGifs.parse_foundGifs($0) } dict[-1132476723] = { return Api.FileLocation.parse_fileLocationToBeDeprecated($0) } - dict[-716006138] = { return Api.Poll.parse_poll($0) } + dict[-2032041631] = { return Api.Poll.parse_poll($0) } dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) } dict[1251338318] = { return Api.InputNotifyPeer.parse_inputNotifyChats($0) } dict[-1311015810] = { return Api.InputNotifyPeer.parse_inputNotifyBroadcasts($0) } @@ -360,8 +360,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-78455655] = { return Api.InputMedia.parse_inputMediaDocumentExternal($0) } dict[-122978821] = { return Api.InputMedia.parse_inputMediaContact($0) } dict[-833715459] = { return Api.InputMedia.parse_inputMediaGeoLive($0) } - dict[-1410741723] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[-1358977017] = { return Api.InputMedia.parse_inputMediaDice($0) } + dict[261416433] = { return Api.InputMedia.parse_inputMediaPoll($0) } dict[2134579434] = { return Api.InputPeer.parse_inputPeerEmpty($0) } dict[2107670217] = { return Api.InputPeer.parse_inputPeerSelf($0) } dict[396093539] = { return Api.InputPeer.parse_inputPeerChat($0) } diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 835ca774a7..583277c93b 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -2028,13 +2028,13 @@ public extension Api { } public enum PollResults: TypeConstructorDescription { - case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?) + case pollResults(flags: Int32, results: [Api.PollAnswerVoters]?, totalVoters: Int32?, recentVoters: [Int32]?, solution: String?, solutionEntities: [Api.MessageEntity]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .pollResults(let flags, let results, let totalVoters, let recentVoters): + case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities): if boxed { - buffer.appendInt32(-932174686) + buffer.appendInt32(-1159937629) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) @@ -2048,14 +2048,20 @@ public extension Api { for item in recentVoters! { serializeInt32(item, buffer: buffer, boxed: false) }} + if Int(flags) & Int(1 << 4) != 0 {serializeString(solution!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(solutionEntities!.count)) + for item in solutionEntities! { + item.serialize(buffer, true) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .pollResults(let flags, let results, let totalVoters, let recentVoters): - return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters)]) + case .pollResults(let flags, let results, let totalVoters, let recentVoters, let solution, let solutionEntities): + return ("pollResults", [("flags", flags), ("results", results), ("totalVoters", totalVoters), ("recentVoters", recentVoters), ("solution", solution), ("solutionEntities", solutionEntities)]) } } @@ -2072,12 +2078,20 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } + var _5: String? + if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } + var _6: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4) + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.PollResults.pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4, solution: _5, solutionEntities: _6) } else { return nil @@ -9162,13 +9176,13 @@ public extension Api { } public enum Poll: TypeConstructorDescription { - case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer]) + case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .poll(let id, let flags, let question, let answers): + case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate): if boxed { - buffer.appendInt32(-716006138) + buffer.appendInt32(-2032041631) } serializeInt64(id, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false) @@ -9178,14 +9192,16 @@ public extension Api { for item in answers { item.serialize(buffer, true) } + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(closePeriod!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {serializeInt32(closeDate!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .poll(let id, let flags, let question, let answers): - return ("poll", [("id", id), ("flags", flags), ("question", question), ("answers", answers)]) + case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate): + return ("poll", [("id", id), ("flags", flags), ("question", question), ("answers", answers), ("closePeriod", closePeriod), ("closeDate", closeDate)]) } } @@ -9200,12 +9216,18 @@ public extension Api { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PollAnswer.self) } + var _5: Int32? + if Int(_2!) & Int(1 << 4) != 0 {_5 = reader.readInt32() } + var _6: Int32? + if Int(_2!) & Int(1 << 5) != 0 {_6 = 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.Poll.poll(id: _1!, flags: _2!, question: _3!, answers: _4!) + let _c5 = (Int(_2!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_2!) & Int(1 << 5) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.Poll.poll(id: _1!, flags: _2!, question: _3!, answers: _4!, closePeriod: _5, closeDate: _6) } else { return nil @@ -10488,8 +10510,8 @@ public extension Api { case inputMediaDocumentExternal(flags: Int32, url: String, ttlSeconds: Int32?) case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) case inputMediaGeoLive(flags: Int32, geoPoint: Api.InputGeoPoint, period: Int32?) - case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?) case inputMediaDice + case inputMediaPoll(flags: Int32, poll: Api.Poll, correctAnswers: [Buffer]?, solution: String?, solutionEntities: [Api.MessageEntity]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -10625,9 +10647,15 @@ public extension Api { geoPoint.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)} break - case .inputMediaPoll(let flags, let poll, let correctAnswers): + case .inputMediaDice: if boxed { - buffer.appendInt32(-1410741723) + buffer.appendInt32(-1358977017) + } + + break + case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities): + if boxed { + buffer.appendInt32(261416433) } serializeInt32(flags, buffer: buffer, boxed: false) poll.serialize(buffer, true) @@ -10636,12 +10664,12 @@ public extension Api { for item in correctAnswers! { serializeBytes(item, buffer: buffer, boxed: false) }} - break - case .inputMediaDice: - if boxed { - buffer.appendInt32(-1358977017) - } - + if Int(flags) & Int(1 << 1) != 0 {serializeString(solution!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(solutionEntities!.count)) + for item in solutionEntities! { + item.serialize(buffer, true) + }} break } } @@ -10676,10 +10704,10 @@ public extension Api { return ("inputMediaContact", [("phoneNumber", phoneNumber), ("firstName", firstName), ("lastName", lastName), ("vcard", vcard)]) case .inputMediaGeoLive(let flags, let geoPoint, let period): return ("inputMediaGeoLive", [("flags", flags), ("geoPoint", geoPoint), ("period", period)]) - case .inputMediaPoll(let flags, let poll, let correctAnswers): - return ("inputMediaPoll", [("flags", flags), ("poll", poll), ("correctAnswers", correctAnswers)]) case .inputMediaDice: return ("inputMediaDice", []) + case .inputMediaPoll(let flags, let poll, let correctAnswers, let solution, let solutionEntities): + return ("inputMediaPoll", [("flags", flags), ("poll", poll), ("correctAnswers", correctAnswers), ("solution", solution), ("solutionEntities", solutionEntities)]) } } @@ -10967,6 +10995,9 @@ public extension Api { return nil } } + public static func parse_inputMediaDice(_ reader: BufferReader) -> InputMedia? { + return Api.InputMedia.inputMediaDice + } public static func parse_inputMediaPoll(_ reader: BufferReader) -> InputMedia? { var _1: Int32? _1 = reader.readInt32() @@ -10978,19 +11009,24 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) } } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputMedia.inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3) + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputMedia.inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3, solution: _4, solutionEntities: _5) } else { return nil } } - public static func parse_inputMediaDice(_ reader: BufferReader) -> InputMedia? { - return Api.InputMedia.inputMediaDice - } } public enum InputPeer: TypeConstructorDescription { diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 30bf7812e2..7ce03aebc6 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -3963,21 +3963,6 @@ public extension Api { } } public struct stats { - public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1421720550) - serializeInt32(flags, buffer: buffer, boxed: false) - channel.serialize(buffer, true) - return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in - let reader = BufferReader(buffer) - var result: Api.stats.BroadcastStats? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.stats.BroadcastStats - } - return result - }) - } - public static func loadAsyncGraph(flags: Int32, token: String, x: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() buffer.appendInt32(1646092192) @@ -3993,6 +3978,22 @@ public extension Api { return result }) } + + public static func getBroadcastStats(flags: Int32, channel: Api.InputChannel, tzOffset: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-433058374) + serializeInt32(flags, buffer: buffer, boxed: false) + channel.serialize(buffer, true) + serializeInt32(tzOffset, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stats.getBroadcastStats", parameters: [("flags", flags), ("channel", channel), ("tzOffset", tzOffset)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastStats? in + let reader = BufferReader(buffer) + var result: Api.stats.BroadcastStats? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stats.BroadcastStats + } + return result + }) + } } public struct auth { public static func checkPhone(phoneNumber: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift index 6095198845..8190fedbf4 100644 --- a/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/AccountStateManagementUtils.swift @@ -2363,7 +2363,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } if let apiPoll = apiPoll { switch apiPoll { - case let .poll(id, flags, question, answers): + case let .poll(id, flags, question, answers, closePeriod, _): let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -2376,7 +2376,7 @@ func replayFinalState(accountManager: AccountManager, postbox: Postbox, accountP } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0) + updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) } } updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin) diff --git a/submodules/TelegramCore/Sources/ChannelStatistics.swift b/submodules/TelegramCore/Sources/ChannelStatistics.swift index 97d5651de8..4d84b12059 100644 --- a/submodules/TelegramCore/Sources/ChannelStatistics.swift +++ b/submodules/TelegramCore/Sources/ChannelStatistics.swift @@ -202,10 +202,10 @@ private func requestStats(postbox: Postbox, network: Network, datacenterId: Int3 signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) |> castError(MTRpcError.self) |> mapToSignal { worker in - return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)) + return worker.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0)) } } else { - signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel)) + signal = network.request(Api.functions.stats.getBroadcastStats(flags: flags, channel: inputChannel, tzOffset: 0)) } return signal diff --git a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift index ae508078d9..fc13a3b243 100644 --- a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift @@ -175,9 +175,15 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: pollMediaFlags |= 1 << 0 correctAnswers = correctAnswersValue.map { Buffer(data: $0) } } - let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers) + if poll.deadlineTimeout != nil { + pollFlags |= 1 << 4 + } + if poll.results.solution != nil { + pollMediaFlags |= 1 << 1 + } + let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil))) - } else if let dice = media as? TelegramMediaDice { + } else if let _ = media as? TelegramMediaDice { let input = Api.InputMedia.inputMediaDice return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(input, text), reuploadInfo: nil))) } else { diff --git a/submodules/TelegramCore/Sources/Polls.swift b/submodules/TelegramCore/Sources/Polls.swift index 803323a618..42e18ae993 100644 --- a/submodules/TelegramCore/Sources/Polls.swift +++ b/submodules/TelegramCore/Sources/Polls.swift @@ -32,7 +32,7 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI resultPoll = transaction.getMedia(pollId) as? TelegramMediaPoll if let poll = poll { switch poll { - case let .poll(id, flags, question, answers): + case let .poll(id, flags, question, answers, closePeriod, _): let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -45,7 +45,7 @@ public func requestMessageSelectPollOption(account: Account, messageId: MessageI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0) + resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) default: break } @@ -126,7 +126,14 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A pollFlags |= 1 << 0 - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption })), correctAnswers: correctAnswers), replyMarkup: nil, entities: nil, scheduleDate: nil)) + if poll.deadlineTimeout != nil { + pollFlags |= 1 << 4 + } + if poll.results.solution != nil { + pollMediaFlags |= 1 << 1 + } + + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil), replyMarkup: nil, entities: nil, scheduleDate: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/Serialization.swift b/submodules/TelegramCore/Sources/Serialization.swift index ca3b511329..0beda75af6 100644 --- a/submodules/TelegramCore/Sources/Serialization.swift +++ b/submodules/TelegramCore/Sources/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 111 + return 112 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift index 2b229a142e..0677ad342e 100644 --- a/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/StoreMessage_Telegram.swift @@ -327,7 +327,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, flags: parsedFlags), nil) case let .messageMediaPoll(poll, results): switch poll { - case let .poll(id, flags, question, answers): + case let .poll(id, flags, question, answers, closePeriod, _): let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -340,7 +340,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0), nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil) } case let .messageMediaDice(value): return (TelegramMediaDice(value: value), nil) diff --git a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift index 0a80671435..fc12a10e1e 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift @@ -29,10 +29,10 @@ extension TelegramMediaPollOptionVoters { extension TelegramMediaPollResults { init(apiResults: Api.PollResults) { switch apiResults { - case let .pollResults(_, results, totalVoters, recentVoters): + case let .pollResults(_, results, totalVoters, recentVoters, solution, _): self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) } - } ?? []) + } ?? [], solution: solution) } } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 5a33489aa8..8aa80994d1 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -236,4 +236,6 @@ public enum PresentationResourceParameterKey: Hashable { case chatPrincipalThemeEssentialGraphics(hasWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners) case chatPrincipalThemeAdditionalGraphics(isCustomWallpaper: Bool, bubbleCorners: PresentationChatBubbleCorners) + + case chatBubbleLamp(incoming: Bool) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index c639d5bfa1..ad8cf91500 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -955,4 +955,10 @@ public struct PresentationResourcesChat { return mediaBubbleCornerImage(incoming: incoming, radius: mainRadius, inset: inset) }) } + + public static func chatBubbleLamp(_ theme: PresentationTheme, incoming: Bool) -> UIImage? { + return theme.image(PresentationResourceParameterKey.chatBubbleLamp(incoming: incoming), { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Lamp"), color: incoming ? theme.chat.message.incoming.accentControlColor : theme.chat.message.outgoing.accentControlColor) + }) + } } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json new file mode 100644 index 0000000000..56cf3cd8f9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_lamp (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/ic_lamp (1).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/Lamp.imageset/ic_lamp (1).pdf new file mode 100644 index 0000000000000000000000000000000000000000..59ba75417fd25b4a4fff4f16d2b94e0660794fe8 GIT binary patch literal 4399 zcmai&2{=^k`^PPlWfCe8DJNM{F*C+m*_SbrHL^2i#?FYblO@ZGk-b47Ysp%p5+O?@ zUPJb5$-ZX^Ve%W*`+s}?@Be@O&ULPHp6h<@^Sz$)-1qmoKIaJPs;Y~^#Nc4Trm3~5 zg-7cTJDZxp5&#sy**Jo)UIidpSXVo|Js?R9=>rgT2WLFioqBgh;jyY%G!BCW%|2GCE@MCPC0B3`zx+b^oywk0faGmLW8 zeBf%$>zd}>h{7&(_p${;SM@#-hD%dNm-A^;`EQ}xFyRT$Ya4vA3@r^7idu&Ax(tDx zj7aP<7Dp=Ds_{maaST6r*cpY$95egWy=(eh@}Ao&S6^p46X)G#Zu=dv_PcNdbTKH;a6yn>uHCKg9#@@XNRJTLi{E7xgEX+c0QoR($`MK zR~bGjtLb(-*gwn&(bbY`q{&c>M51R3@88w*JX^T~+7$RRzCA=l!!T6j%{^Aej1mhj z49Cp{G8h{aLR>s%RE_m@&=(o-H9^(bn@5BDUtf zL?(J&*Jb`b#T$DS>zK*Bx%i+!qm}0=O48D!CgvL>R|Fxu{9KMkUf^}u<$qp==<)X#tl&J|Cc_W%w|rGH?SUwQ}gf0<0z z9fvl+;sGag zR#&Q10f;Kr%K?qmM=1Yy4`-jS7~#P_E!I+&wi~K@*oW`-G(u-lZjzzG?e! zb%SGPZW~bK-udkst_m%ZGnELLCS%Tv#gH0KnoR-I`=?2@DTXu^E)0rOt`aIEfbqQvj`uKSzL7Xpv`g;yErSon-z+CUl$>}mDaYrYjmZ6OCjVO@&p{K zHdXMb;F@ZCI}0n3jG=6??@zpub@;Qj@zR5=H(aaXvxa)J$;@F<_SNB6LyY6aFyOGD z-tnN5Qo@Yfw=^-e$0>fo>E0K)vaB?&%=ja7o75wM|7tGjU;CHpJ?Mo8MJmSRt zzR&FN9X5}49oJIgH<;dJiNm-H=3L^=9Sd`=r#lwUh<^x{2oZzQU9vvH8w_`&9|dT_ zf;HIb@6n5C&^aiAo+-%>(14X-1GHXu9NlPoLi{>G+M!Zz4CbJlZ;rSfW(h_pf+Om$ zL1{-r9GF7Cu%F0LdJ@l>$Cjqt$9~F`$yY^5SB8WBtx8lpd!pjEPNByqFWh;Ot)eTp zaSqQ@#bj}3=OJS!Ai+SP4GcS)O4p4#GC=bpc(D8mFB77n?USm{i3f~aZ-&M-D>>y^ zUp8=!pZY>`Dcq_diAnJt6MdJuG1IA4RpoeN0otQyof7!-=nB=+<5}}gy^7UHxHU=7 z5SfiiJ;~Y3JrQX_7`HJ(_1S1bdsES=Mp|6cQnu{ykZ+;+HoZ>^jD?F8yqKkq-j5Y+ zGO~`dHY+DSr5`{oUg2eJdBgpg*X?%YNpo>m$x_fuwthl&@IvE}aiLY8*B3@PRzlh8 zJ-*lkoUH{1@dlk4i{opq6OLy#1v!PXoxKMLI1Aih=V6**qJ$l7kkAWxtb^x|VE1h@ zw-I@IcR=Yil2=GOi31rSz@;xBl2#FN1L??bAtWbMb2h2%@@6m&bGp73a!72__Nngmm6+chcP34Zhi$ z4Q8w$)@$z6zRP_Lj%-$8?MitPe>wE6XMn-reBdK$U{+a z!k=QBLYorzM9UD@%h8+h#N~-j57be}5#>1OxZ5jM6ia?Y_Rb_{SV`BTrY&RGKeA9B1JQ*`d?q3$hm2h+=F4{)KQ^U1S)c=3A{-~sq>h4->E|VdqGMilOMzF? zEy_nYtF^1io2;AsyF$A#M!86z$QstI0le4w>af7xqYJa+l^v^u2}WUm;iuimM`IZ# z9U>h^I&u%QaDCzG5k2YNb!)tm*F8ZDBSx_Dam(&897>rmUD#>A-XA;T)#TDNG9Wid zsocHDc8x6o5z~_VC~1a=eAx02V%g1vn`JK(w`^7%S02e-}G5cKUa9Kr*0W=2>taNF8LvD1TU-`Pd;(P-8u9eRU`wG>zmXI5(=lZDCg$+;r%!mh$U9l|QME4q~hQPJfWE#Pv(HIPP$ zT&PObdxzd-!{a%MT?ty}Jxpw?Q}2G)`l>+?xG#9ewLm_X~dRP;y6U)x9s|A)Al*~vyR|! z;=-6o;LUF%?VDjsHTj$QyWXF?n7oHa=Bn0>(`-yE2RlnYS(Hq*ujQ=0tiHKeYlJep zWzFUghl#5YfBdXQv&<3E`)EFIK7=POeY$4e|K(D~$Cjb~x-plaZrWJ-w~X>6a9=jd`}Uf0bNFgZ6j$Wsn|S%E zipdWdrRAlgZ6U-m%hgxzg+n#Fdxpd!yHZ(;D}fn-HA@kc*uFE>nfI&~Z#&FAUva6K zb0bmiO_bcNJrp>zW3dYv-K+|ny1Sooiav&6XIZS)f8TOgZZ2??(#2TFW~tEbCquEP zpcf}{guyx&GM^}vP?ubfLwFHcu?W_0MV&xzX3ArApM_;Dkwb48E5ws-#vbD``=g&`|SZLmSa58 z)bHK%E-18)0bqo6_i(_u0x+l;Oi~OEn4eenba2K1Fj-Y;DRU7(&lBa2_XDU1|AYE{ z@FG-lr+yjZsSl}AF&une^|Clr914|y!eMZjgap(S3KgcF)V%9B%s~q9|EK)DMjv;q zEf@;GU|{I~9DtOhBwP}(1%AcgGLqEIJvac@zhY1s81?o38G}oUQw#IY7!)c_-OB$J zla~E=Oh)$KdQ$&Ko~$IbA^xccmy-UM9$W_c+nVw2CiYr^eH@Nj z@q?;T|3)BPZE@7{{~W7Q8{jGiB`yt@mW9H_WuR!7I7&(eg|Zcw61PR8C81bZtUUO? ar~GUn4?MM&KQ>x|x*K3YK{Xw9@P7bcuCFuz literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fccc0b8e67..ca55b5e8a9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1637,6 +1637,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected() return; } + if false { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: "controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers"), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) + return; + } #endif controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers @@ -1654,7 +1658,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self, let resultPoll = resultPoll else { return } - guard let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else { + guard let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else { return } @@ -1686,6 +1690,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.selectPollOptionFeedback?.error() itemNode.animateQuizInvalidOptionSelected() + + if let solution = resultPoll.results.solution { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: solution), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) + } } } } @@ -1930,6 +1938,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } strongSelf.presentPollCreation(isQuiz: isQuiz) + }, displayPollSolution: { [weak self] text in + guard let strongSelf = self else { + return + } + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: text), elevatedLayout: true, action: { _ in return false }), in: .window(.root)) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 7b01b38fdd..7468326661 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -107,6 +107,7 @@ public final class ChatControllerInteraction { let dismissReplyMarkupMessage: (Message) -> Void let openMessagePollResults: (MessageId, Data) -> Void let openPollCreation: (Bool?) -> Void + let displayPollSolution: (String) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -121,7 +122,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -167,6 +168,7 @@ public final class ChatControllerInteraction { self.requestSelectMessagePollOptions = requestSelectMessagePollOptions self.requestOpenMessagePollResults = requestOpenMessagePollResults self.openPollCreation = openPollCreation + self.displayPollSolution = displayPollSolution self.openAppStorePage = openAppStorePage self.displayMessageTooltip = displayMessageTooltip self.seekToTimecode = seekToTimecode @@ -220,6 +222,7 @@ public final class ChatControllerInteraction { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 9fdf553223..7ed41a899f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -518,7 +518,9 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: var activePoll: TelegramMediaPoll? for media in message.media { if let poll = media as? TelegramMediaPoll, !poll.isClosed, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { - activePoll = poll + if !isPollEffectivelyClosed(message: message, poll: poll) { + activePoll = poll + } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index ceac6d1466..182d48cffd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -2544,13 +2544,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } case let .tooltip(text, node, rect): if let item = self.item { - return .action({ + return .optionalAction({ let _ = item.controllerInteraction.displayMessageTooltip(item.message.id, text, node, rect) }) } case let .openPollResults(option): if let item = self.item { - return .action({ + return .optionalAction({ item.controllerInteraction.openMessagePollResults(item.message.id, option) }) } diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index dbe07bbc9d..b1cf8fb78b 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -12,6 +12,28 @@ import AccountContext import AvatarNode import TelegramPresentationData +func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool { + if poll.isClosed { + return true + } else if let deadlineTimeout = poll.deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud { + let startDate: Int32 + if let forwardInfo = message.forwardInfo { + startDate = forwardInfo.date + } else { + startDate = message.timestamp + } + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if timestamp >= startDate + deadlineTimeout { + return true + } else { + return false + } + } else { + return false + } +} + private struct PercentCounterItem: Comparable { var index: Int = 0 var percent: Int = 0 @@ -187,7 +209,6 @@ private final class ChatMessagePollOptionRadioNode: ASDisplayNode { func update(staticColor: UIColor, animatedColor: UIColor, fillColor: UIColor, foregroundColor: UIColor, isSelectable: Bool, isAnimating: Bool) { var updated = false let shouldHaveBeenAnimating = self.shouldBeAnimating - let wasAnimating = self.isAnimating if !staticColor.isEqual(self.staticColor) { self.staticColor = staticColor updated = true @@ -779,9 +800,48 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private let labelsFont = Font.regular(14.0) +private final class SolutionButtonNode: HighlightableButtonNode { + private let pressed: () -> Void + private let iconNode: ASImageNode + + private var theme: PresentationTheme? + private var incoming: Bool? + + init(pressed: @escaping () -> Void) { + self.pressed = pressed + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.iconNode) + + self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside) + } + + @objc private func pressedEvent() { + self.pressed() + } + + func update(size: CGSize, theme: PresentationTheme, incoming: Bool) { + if self.theme !== theme || self.incoming != incoming { + self.theme = theme + self.incoming = incoming + self.iconNode.image = PresentationResourcesChat.chatBubbleLamp(theme, incoming: incoming) + } + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + } + } +} + class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let typeNode: TextNode + private var timerNode: PollBubbleTimerNode? + private var solutionButtonNode: SolutionButtonNode? private let avatarsNode: MergedAvatarsNode private let votersNode: TextNode private let buttonSubmitInactiveTextNode: TextNode @@ -918,14 +978,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] - var hasSelectedOptions = false for optionNode in self.optionNodes { if let option = optionNode.option { previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode) } - if let isChecked = optionNode.radioNode?.isChecked, isChecked { - hasSelectedOptions = true - } } return { item, layoutConstants, _, _, _ in @@ -1020,7 +1076,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - if let poll = poll, poll.isClosed { + if let poll = poll, isPollEffectivelyClosed(message: message, poll: poll) { typeText = item.presentationData.strings.MessagePoll_LabelClosed } else if let poll = poll { switch poll.kind { @@ -1090,13 +1146,20 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + let isClosed: Bool + if let poll = poll { + isClosed = isPollEffectivelyClosed(message: message, poll: poll) + } else { + isClosed = false + } + var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = [] if let poll = poll { var optionVoterCount: [Int: Int32] = [:] var maxOptionVoterCount: Int32 = 0 var totalVoterCount: Int32 = 0 let voters: [TelegramMediaPollOptionVoters]? - if poll.isClosed { + if isClosed { voters = poll.results.voters ?? [] } else { voters = poll.results.voters @@ -1109,7 +1172,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } totalVoterCount = totalVoters - if didVote || poll.isClosed { + if didVote || isClosed { for i in 0 ..< poll.options.count { inner: for optionVoters in voters { if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier { @@ -1142,10 +1205,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let count = optionVoterCount[i] { if maxOptionVoterCount != 0 && totalVoterCount != 0 { optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count) - } else if poll.isClosed { + } else if isClosed { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) } - } else if poll.isClosed { + } else if isClosed { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) } let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) @@ -1157,7 +1220,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width = max(boundingSize.width, min(270.0, constrainedSize.width)) var canVote = false - if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !poll.isClosed { + if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allScheduled.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { var hasVoted = false if let voters = poll.results.voters { for voter in voters { @@ -1304,6 +1367,130 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) strongSelf.typeNode.frame = typeFrame + + let deadlineTimeout = poll?.deadlineTimeout + var displayDeadline = true + var hasSelected = false + + if let poll = poll { + if let voters = poll.results.voters { + for voter in voters { + if voter.selected { + displayDeadline = false + hasSelected = true + break + } + } + } + } + + if let deadlineTimeout = deadlineTimeout, !isClosed { + var endDate: Int32? + + if message.id.namespace == Namespaces.Message.Cloud { + let startDate: Int32 + if let forwardInfo = message.forwardInfo { + startDate = forwardInfo.date + } else { + startDate = message.timestamp + } + endDate = startDate + deadlineTimeout + } + + let timerNode: PollBubbleTimerNode + if let current = strongSelf.timerNode { + timerNode = current + let timerTransition: ContainedViewLayoutTransition + if animation.isAnimated { + timerTransition = .animated(duration: 0.25, curve: .easeInOut) + } else { + timerTransition = .immediate + } + if displayDeadline { + timerTransition.updateAlpha(node: timerNode, alpha: 1.0) + } else { + timerTransition.updateAlpha(node: timerNode, alpha: 0.0) + } + } else { + timerNode = PollBubbleTimerNode() + strongSelf.timerNode = timerNode + strongSelf.addSubnode(timerNode) + timerNode.reachedTimeout = { + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.requestMessageUpdate(item.message.id) + } + + let timerTransition: ContainedViewLayoutTransition + if animation.isAnimated { + timerTransition = .animated(duration: 0.25, curve: .easeInOut) + } else { + timerTransition = .immediate + } + if displayDeadline { + timerNode.alpha = 0.0 + timerTransition.updateAlpha(node: timerNode, alpha: 1.0) + } else { + timerNode.alpha = 0.0 + } + } + timerNode.update(regularColor: messageTheme.secondaryTextColor, proximityColor: messageTheme.scamColor, timeout: deadlineTimeout, deadlineTimestamp: endDate) + timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize()) + } else if let timerNode = strongSelf.timerNode { + strongSelf.timerNode = nil + + let timerTransition: ContainedViewLayoutTransition + if animation.isAnimated { + timerTransition = .animated(duration: 0.25, curve: .easeInOut) + } else { + timerTransition = .immediate + } + timerTransition.updateAlpha(node: timerNode, alpha: 0.0, completion: { [weak timerNode] _ in + timerNode?.removeFromSupernode() + }) + timerTransition.updateTransformScale(node: timerNode, scale: 0.1) + } + + if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .anonymous = poll.publicity, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) { + let solutionButtonNode: SolutionButtonNode + if let current = strongSelf.solutionButtonNode { + solutionButtonNode = current + } else { + solutionButtonNode = SolutionButtonNode(pressed: { + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.displayPollSolution(solution) + }) + strongSelf.solutionButtonNode = solutionButtonNode + strongSelf.addSubnode(solutionButtonNode) + + let timerTransition: ContainedViewLayoutTransition + if animation.isAnimated { + timerTransition = .animated(duration: 0.25, curve: .easeInOut) + } else { + timerTransition = .immediate + } + solutionButtonNode.alpha = 0.0 + timerTransition.updateAlpha(node: solutionButtonNode, alpha: 1.0) + } + let buttonSize = CGSize(width: 32.0, height: 32.0) + solutionButtonNode.update(size: buttonSize, theme: item.presentationData.theme.theme, incoming: item.message.flags.contains(.Incoming)) + solutionButtonNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - buttonSize.width + 5.0, y: typeFrame.minY - 16.0), size: buttonSize) + } else if let solutionButtonNode = strongSelf.solutionButtonNode { + let timerTransition: ContainedViewLayoutTransition + if animation.isAnimated { + timerTransition = .animated(duration: 0.25, curve: .easeInOut) + } else { + timerTransition = .immediate + } + timerTransition.updateAlpha(node: solutionButtonNode, alpha: 0.0, completion: { [weak solutionButtonNode] _ in + solutionButtonNode?.removeFromSupernode() + }) + timerTransition.updateTransformScale(node: solutionButtonNode, scale: 0.1) + } + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - mergedImageSize) / 2.0)), size: CGSize(width: mergedImageSize + mergedImageSpacing * 2.0, height: mergedImageSize)) strongSelf.avatarsNode.frame = avatarsFrame strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) @@ -1367,8 +1554,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } + let isClosed = isPollEffectivelyClosed(message: item.message, poll: poll) + var hasResults = false - if poll.isClosed { + if isClosed { hasResults = true hasSelection = false if let totalVoters = poll.results.totalVoters, totalVoters == 0 { @@ -1503,6 +1692,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if self.avatarsNode.isUserInteractionEnabled, !self.avatarsNode.isHidden, self.avatarsNode.frame.contains(point) { return .ignore } + if let solutionButtonNode = self.solutionButtonNode, solutionButtonNode.isUserInteractionEnabled, !solutionButtonNode.isHidden, solutionButtonNode.frame.contains(point) { + return .ignore + } return .none } } @@ -1590,7 +1782,7 @@ private final class MergedAvatarsNode: ASDisplayNode { func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool) { var filteredPeers = peers.map(PeerAvatarReference.init) if filteredPeers.count > 3 { - filteredPeers.dropLast(filteredPeers.count - 3) + let _ = filteredPeers.dropLast(filteredPeers.count - 3) } if filteredPeers != self.peers { self.peers = filteredPeers @@ -1667,7 +1859,6 @@ private final class MergedAvatarsNode: ASDisplayNode { return } - let imageOverlaySpacing: CGFloat = 1.0 context.setBlendMode(.copy) var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index e484f7412e..a2e5de3fd0 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -423,6 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift index 330f8213b5..a29ce96c7e 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControllerNode.swift @@ -122,6 +122,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index e06d71b818..8fd88d80a6 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1527,6 +1527,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift index 182b7119ff..9ffc1cec19 100644 --- a/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/Sources/PeerMediaCollectionController.swift @@ -427,6 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift b/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift new file mode 100644 index 0000000000..7e525edac3 --- /dev/null +++ b/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift @@ -0,0 +1,264 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private func textForTimeout(value: Int) -> String { + //TODO: localize + if value > 60 * 60 { + let hours = value / (60 * 60) + return "\(hours)h" + } else { + let minutes = value / 60 + let seconds = value % 60 + let minutesPadding = minutes < 10 ? "0" : "" + let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + } +} + +private enum ContentState: Equatable { + case clock(UIColor) + case timeout(UIColor, CGFloat) +} + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +final class PollBubbleTimerNode: ASDisplayNode { + private struct Params: Equatable { + var regularColor: UIColor + var proximityColor: UIColor + var timeout: Int32 + var deadlineTimestamp: Int32? + } + + private let hierarchyTrackingNode: HierarchyTrackingNode + private var inHierarchyValue: Bool = false + + private var animator: ConstantDisplayLinkAnimator? + private let textNode: ImmediateTextNode + private let contentNode: ASDisplayNode + private var currentContentState: ContentState? + private var particles: [ContentParticle] = [] + + private var currentParams: Params? + + var reachedTimeout: (() -> Void)? + + override init() { + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + + self.contentNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.contentNode) + + updateInHierarchy = { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.inHierarchyValue = value + strongSelf.animator?.isPaused = value + } + } + + deinit { + self.animator?.invalidate() + } + + func update(regularColor: UIColor, proximityColor: UIColor, timeout: Int32, deadlineTimestamp: Int32?) { + let params = Params( + regularColor: regularColor, + proximityColor: proximityColor, + timeout: timeout, + deadlineTimestamp: deadlineTimestamp + ) + self.currentParams = params + + self.updateValues() + } + + private func updateValues() { + guard let params = self.currentParams else { + return + } + + let fractionalTimeout: Double + + if let deadlineTimestamp = params.deadlineTimestamp { + let fractionalTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + fractionalTimeout = max(0.0, Double(deadlineTimestamp) - fractionalTimestamp) + } else { + fractionalTimeout = Double(params.timeout) + } + + let timeout = Int(round(fractionalTimeout)) + + let proximityInterval: Double = 5.0 + let timerInterval: Double = 60.0 + + let isProximity = timeout <= Int(proximityInterval) + let isTimer = timeout <= Int(timerInterval) + + let color = isProximity ? params.proximityColor : params.regularColor + self.textNode.attributedText = NSAttributedString(string: textForTimeout(value: timeout), font: Font.regular(14.0), textColor: color) + let textSize = textNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + self.textNode.frame = CGRect(origin: CGPoint(x: -22.0 - textSize.width, y: 0.0), size: textSize) + + let contentState: ContentState + if isTimer { + var fraction: CGFloat = 1.0 + if fractionalTimeout <= timerInterval { + fraction = CGFloat(fractionalTimeout) / min(CGFloat(timerInterval), CGFloat(params.timeout)) + } + fraction = max(0.0, min(0.99, fraction)) + contentState = .timeout(color, 1.0 - fraction) + } else { + contentState = .clock(color) + } + + if self.currentContentState != contentState { + self.currentContentState = contentState + let image: UIImage? + + let diameter: CGFloat = 14.0 + let inset: CGFloat = 8.0 + let lineWidth: CGFloat = 1.2 + + switch contentState { + case let .clock(color): + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let clockFrame = CGRect(origin: CGPoint(x: (size.width - diameter) / 2.0, y: (size.height - diameter) / 2.0), size: CGSize(width: diameter, height: diameter)) + context.strokeEllipse(in: clockFrame.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + + context.move(to: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: clockFrame.minY + 4.0)) + context.strokePath() + + let topWidth: CGFloat = 4.0 + context.move(to: CGPoint(x: size.width / 2.0 - topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0 + topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.strokePath() + }) + case let .timeout(color, fraction): + let timestamp = CACurrentMediaTime() + + let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0) + let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0 + + let startAngle: CGFloat = -CGFloat.pi / 2.0 + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + + for particle in self.particles { + let size: CGFloat = 1.15 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + }) + } + + self.contentNode.contents = image?.cgImage + if let image = image { + self.contentNode.frame = CGRect(origin: CGPoint(x: -image.size.width, y: -3.0), size: image.size) + } + } + + if let reachedTimeout = self.reachedTimeout, fractionalTimeout <= .ulpOfOne { + reachedTimeout() + } + + if fractionalTimeout <= .ulpOfOne { + self.animator?.invalidate() + self.animator = nil + } else { + if self.animator == nil { + let animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateValues() + }) + self.animator = animator + animator.isPaused = self.inHierarchyValue + } + } + } +} diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 4a7e627a71..0dec75d964 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -18,25 +18,30 @@ private final class PollResultsControllerArguments { let collapseOption: (Data) -> Void let expandOption: (Data) -> Void let openPeer: (RenderedPeer) -> Void + let expandSolution: () -> Void - init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void) { + init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (RenderedPeer) -> Void, expandSolution: @escaping () -> Void) { self.context = context self.collapseOption = collapseOption self.expandOption = expandOption self.openPeer = openPeer + self.expandSolution = expandSolution } } private enum PollResultsSection { case text + case solution case option(Int) var rawValue: Int32 { switch self { case .text: return 0 + case .solution: + return 1 case let .option(index): - return 1 + Int32(index) + return 2 + Int32(index) } } } @@ -45,6 +50,8 @@ private enum PollResultsEntryId: Hashable { case text case optionPeer(Int, Int) case optionExpand(Int) + case solutionHeader + case solutionText } private enum PollResultsItemTag: ItemListItemTag, Equatable { @@ -63,6 +70,8 @@ private enum PollResultsEntry: ItemListNodeEntry { case text(String) case optionPeer(optionId: Int, index: Int, peer: RenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool) + case solutionHeader(String) + case solutionText(String) var section: ItemListSectionId { switch self { @@ -72,6 +81,8 @@ private enum PollResultsEntry: ItemListNodeEntry { return PollResultsSection.option(optionPeer.optionId).rawValue case let .optionExpand(optionExpand): return PollResultsSection.option(optionExpand.optionId).rawValue + case .solutionHeader, .solutionText: + return PollResultsSection.solution.rawValue } } @@ -83,6 +94,10 @@ private enum PollResultsEntry: ItemListNodeEntry { return .optionPeer(optionPeer.optionId, optionPeer.index) case let .optionExpand(optionExpand): return .optionExpand(optionExpand.optionId) + case .solutionHeader: + return .solutionHeader + case .solutionText: + return .solutionText } } @@ -95,10 +110,34 @@ private enum PollResultsEntry: ItemListNodeEntry { default: return true } + case .solutionHeader: + switch rhs { + case .text: + return false + case .solutionHeader: + return false + default: + return true + } + case .solutionText: + switch rhs { + case .text: + return false + case .solutionHeader: + return false + case .solutionText: + return false + default: + return true + } case let .optionPeer(lhsOptionPeer): switch rhs { case .text: return false + case .solutionHeader: + return false + case .solutionText: + return false case let .optionPeer(rhsOptionPeer): if lhsOptionPeer.optionId == rhsOptionPeer.optionId { return lhsOptionPeer.index < rhsOptionPeer.index @@ -116,6 +155,10 @@ private enum PollResultsEntry: ItemListNodeEntry { switch rhs { case .text: return false + case .solutionHeader: + return false + case .solutionText: + return false case let .optionPeer(rhsOptionPeer): if lhsOptionExpand.optionId == rhsOptionPeer.optionId { return false @@ -137,6 +180,10 @@ private enum PollResultsEntry: ItemListNodeEntry { switch self { case let .text(text): return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section) + case let .solutionHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .solutionText(text): + return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks) case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { arguments.collapseOption(opaqueIdentifier) @@ -156,6 +203,7 @@ private enum PollResultsEntry: ItemListNodeEntry { private struct PollResultsControllerState: Equatable { var expandedOptions: [Data: Int] = [:] + var isSolutionExpanded: Bool = false } private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { @@ -171,12 +219,18 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po entries.append(.text(poll.text)) + if let solution = poll.results.solution, !solution.isEmpty { + //TODO:localize + entries.append(.solutionHeader("EXPLANATION")) + entries.append(.solutionText(solution)) + } + var optionVoterCount: [Int: Int32] = [:] let totalVoterCount = poll.results.totalVoters ?? 0 var optionPercentage: [Int] = [] if totalVoterCount != 0 { - if let voters = poll.results.voters, let totalVoters = poll.results.totalVoters { + if let voters = poll.results.voters, let _ = poll.results.totalVoters { for i in 0 ..< poll.options.count { inner: for optionVoters in voters { if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier { @@ -215,7 +269,6 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po } } else { if let optionState = resultsState.options[option.opaqueIdentifier], !optionState.peers.isEmpty { - var hasMore = false let optionExpandedAtCount = state.expandedOptions[option.opaqueIdentifier] let peers = optionState.peers @@ -307,6 +360,8 @@ public func pollResultsController(context: AccountContext, messageId: MessageId, pushControllerImpl?(controller) } } + }, expandSolution: { + }) let previousWasEmpty = Atomic(value: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index c27dd0ff0a..2ae89b4e0e 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1135,6 +1135,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in }, openPollCreation: { _ in + }, displayPollSolution: { _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 8dd4662294..093045f550 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -12,6 +12,7 @@ public enum UndoOverlayContent { case hidArchive(title: String, text: String, undo: Bool) case revealedArchive(title: String, text: String, undo: Bool) case succeed(text: String) + case info(text: String) case emoji(path: String, text: String) case swipeToReply(title: String, text: String) case actionSucceeded(title: String, text: String, cancel: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index e5308845aa..1020ba7b12 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -137,6 +137,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .info(text): + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: "anim_infotip", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + self.animatedStickerNode = nil + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 2 + displayUndo = false + self.originalRemainingSeconds = max(5, min(8, text.count / 14)) case let .actionSucceeded(title, text, cancel): self.iconNode = nil self.iconCheckNode = nil @@ -296,6 +309,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.panelWrapperNode.addSubnode(self.timerTextNode) case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified: break + case .info: + self.isUserInteractionEnabled = false } self.statusNode.flatMap(self.panelWrapperNode.addSubnode) self.iconNode.flatMap(self.panelWrapperNode.addSubnode) @@ -350,7 +365,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } @objc private func undoButtonPressed() { - self.action(.undo) + let _ = self.action(.undo) self.dismiss() } @@ -359,7 +374,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.remainingSeconds -= 1 } if self.remainingSeconds == 0 { - self.action(.commit) + let _ = self.action(.commit) self.dismiss() } else { if !self.timerTextNode.bounds.size.width.isZero, let snapshot = self.timerTextNode.view.snapshotContentTree() {