From 4bfaf50de83f5c2f99a2493b96672dca2013cfb3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 10:48:34 +0800 Subject: [PATCH 01/19] Remove vfsoverlay option (cherry picked from commit 6d131ccd2fa60617c8e7d4e1ce758d3b39922f8e) --- .bazelrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.bazelrc b/.bazelrc index 5ecdbaff0d..43ccf92e1f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -29,7 +29,6 @@ build --features=debug_prefix_map_pwd_is_dot build --features=swift.cacheable_swiftmodules build --features=swift.debug_prefix_map build --features=swift.enable_vfsoverlays -build --features=swift.vfsoverlay build --strategy=Genrule=standalone build --spawn_strategy=standalone From 0a5d8fcb10c57577b10034a42a987dc77e432cae Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 20:26:44 +0800 Subject: [PATCH 02/19] Draw strikethrough and underline manually on iOS 18 (cherry picked from commit d70a0cf0e09db603cf5c5e41582f6a20a3ac4b9d) --- .../Sources/InteractiveTextComponent.swift | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 2868bce605..4a521443bf 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -112,13 +112,14 @@ private final class InteractiveTextNodeLine { let isTruncated: Bool let isRTL: Bool var strikethroughs: [InteractiveTextNodeStrikethrough] + var underlines: [InteractiveTextNodeStrikethrough] var spoilers: [InteractiveTextNodeSpoiler] var spoilerWords: [InteractiveTextNodeSpoiler] var embeddedItems: [InteractiveTextNodeEmbeddedItem] var attachments: [InteractiveTextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], underlines: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line self.constrainedWidth = constrainedWidth self.frame = frame @@ -129,6 +130,7 @@ private final class InteractiveTextNodeLine { self.isTruncated = isTruncated self.isRTL = isRTL self.strikethroughs = strikethroughs + self.underlines = underlines self.spoilers = spoilers self.spoilerWords = spoilerWords self.embeddedItems = embeddedItems @@ -1452,6 +1454,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: false, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1493,6 +1496,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: isRTL && segment.blockQuote == nil, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1551,6 +1555,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1605,6 +1610,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1736,6 +1742,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) let x = lowerX < upperX ? lowerX : upperX line.strikethroughs.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) + } else if let _ = attributes[NSAttributedString.Key.underlineStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + line.underlines.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) } if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { @@ -2090,6 +2101,14 @@ final class TextContentItem { } } +private let drawUnderlinesManually: Bool = { + if #available(iOS 18.0, *) { + return true + } else { + return false + } +}() + final class TextContentItemLayer: SimpleLayer { final class Params { let item: TextContentItem @@ -2322,6 +2341,46 @@ final class TextContentItemLayer: SimpleLayer { } } + if drawUnderlinesManually { + if !line.strikethroughs.isEmpty { + for strikethrough in line.strikethroughs { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.midY, width: frame.width, height: 1.0)) + } + } + + if !line.underlines.isEmpty { + for strikethrough in line.underlines { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.maxY - 2.0, width: frame.width, height: 1.0)) + } + } + } + if let (additionalTrailingLine, _) = line.additionalTrailingLine { context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent) From 9d7d4bd245d6b86789f2e34cdb946e31a50448b5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 20:29:28 +0800 Subject: [PATCH 03/19] Re-disable video chats for now --- submodules/TelegramCallsUI/Sources/VoiceChatController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index db52366c58..3d6d2ebb00 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { - var useV2 = true + var useV2 = false + if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] { + useV2 = true + } if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } From 1d1ea447ad3fdbf9483ce337c532cf0793f3ab3a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 27 Sep 2024 17:03:21 +0400 Subject: [PATCH 04/19] Various fixes --- .../Sources/ChatEntityKeyboardInputNode.swift | 4 ++-- .../Sources/GiftSetupScreen.swift | 3 ++- .../Animations/ChatListNoResults.tgs | Bin 8590 -> 11826 bytes 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 4bfea4558a..71c184ae51 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -468,7 +468,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { |> distinctUntilChanged } - public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { + public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false) { self.context = context self.currentInputData = currentInputData self.defaultToEmojiTab = defaultToEmojiTab @@ -689,7 +689,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") { + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") && !forceHasPremium { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index a44ba40f60..5f939cebbe 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -914,7 +914,8 @@ final class GiftSetupScreenComponent: Component { useOpaqueTheme: true, interaction: self.inputMediaInteraction, chatPeerId: nil, - stateContext: self.inputMediaNodeStateContext + stateContext: self.inputMediaNodeStateContext, + forceHasPremium: true ) inputMediaNode.clipsToBounds = true diff --git a/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs b/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs index 62a21279fb766aa9aa25ddc077936f2f51f34031..1cde51426d640fc5ed44f94df27a589eacc73062 100644 GIT binary patch literal 11826 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 literal 8590 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 From 9f11ee1c5a1d40c9e92a772e689c2bbd7c6c1691 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 4 Oct 2024 18:03:06 +0400 Subject: [PATCH 05/19] Update build system --- build-system/Make/Make.py | 60 +++------------------------------------ 1 file changed, 4 insertions(+), 56 deletions(-) diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 971c8c2d80..bd2d011b0c 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -148,18 +148,7 @@ class BazelCommandLine: self.disable_provisioning_profiles = True def set_configuration(self, configuration): - if configuration == 'debug_universal': - self.configuration_args = [ - # bazel debug build configuration - '-c', 'dbg', - - # Build universal binaries. - '--ios_multi_cpus=armv7,arm64', - - # Always build universal Watch binaries. - '--watchos_cpus=arm64_32' - ] + self.common_debug_args - elif configuration == 'debug_arm64': + if configuration == 'debug_arm64': self.configuration_args = [ # bazel debug build configuration '-c', 'dbg', @@ -189,16 +178,6 @@ class BazelCommandLine: # Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required. '--ios_multi_cpus=sim_arm64', - # Always build universal Watch binaries. - '--watchos_cpus=arm64_32' - ] + self.common_debug_args - elif configuration == 'debug_armv7': - self.configuration_args = [ - # bazel debug build configuration - '-c', 'dbg', - - '--ios_multi_cpus=armv7', - # Always build universal Watch binaries. '--watchos_cpus=arm64_32' ] + self.common_debug_args @@ -217,41 +196,10 @@ class BazelCommandLine: '--apple_generate_dsym', # Require DSYM files as build output. - '--output_groups=+dsyms' - ] + self.common_release_args - elif configuration == 'release_armv7': - self.configuration_args = [ - # bazel optimized build configuration - '-c', 'opt', + '--output_groups=+dsyms', - # Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required. - '--ios_multi_cpus=armv7', - - # Always build universal Watch binaries. - '--watchos_cpus=arm64_32', - - # Generate DSYM files when building. - '--apple_generate_dsym', - - # Require DSYM files as build output. - '--output_groups=+dsyms' - ] + self.common_release_args - elif configuration == 'release_universal': - self.configuration_args = [ - # bazel optimized build configuration - '-c', 'opt', - - # Build universal binaries. - '--ios_multi_cpus=armv7,arm64', - - # Always build universal Watch binaries. - '--watchos_cpus=arm64_32', - - # Generate DSYM files when building. - '--apple_generate_dsym', - - # Require DSYM files as build output. - '--output_groups=+dsyms' + '--swiftcopt=-num-threads', + '--swiftcopt=0', ] + self.common_release_args else: raise Exception('Unknown configuration {}'.format(configuration)) From 62517683ad2607fa46680cf934af6f8595f189de Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 01:05:29 +0400 Subject: [PATCH 06/19] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 +++ .../Sources/ChatListSearchListPaneNode.swift | 26 +++++++++---------- .../Sources/Node/ChatListNode.swift | 17 +++++++++--- .../Sources/ContactsPeerItem.swift | 14 +++++++++- .../Sources/TelegramEngine/Peers/Peer.swift | 4 +++ .../TelegramEngine/Peers/SearchPeers.swift | 14 +++++++--- .../Sources/ContentReportScreen.swift | 8 ++++-- .../Sources/GiftSetupScreen.swift | 3 +-- 8 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5bf388371a..6d7999efb1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13049,6 +13049,7 @@ Sorry for the inconvenience."; "Report.Title.User" = "Report User"; "Report.Title.Group" = "Report Group"; "Report.Title.Channel" = "Report Channel"; +"Report.Title.Bot" = "Report Bot"; "Report.Comment.Placeholder" = "Add Comment"; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; @@ -13064,3 +13065,6 @@ Sorry for the inconvenience."; "Notification.StarsGift.Stars_1" = "%@ Star"; "Notification.StarsGift.Stars_any" = "%@ Stars"; + +"WebBrowser.AuthChallenge.Title" = "Sign in to %@"; +"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index a522784efb..4f211c7658 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool) case addContact(String, PresentationTheme, PresentationStrings) @@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .localPeerId(peer.id) case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _): return .messageId(message.id, section) @@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery { return true } else { return false @@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case .globalPeer, .message, .addContact: return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer, .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true @@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging): + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { if peer.peer is TelegramUser { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))" } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" } else { @@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { isSavedMessages = true } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil) }, disabledAction: { _ in interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery)) index += 1 numberOfGlobalPeers += 1 } @@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case let .user(user) = peer, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(peer.id) } - case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _): storyStatsIds.append(foundPeer.peer.id) if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) @@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) - case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): - mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false) + case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery): + mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery) case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _): mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false) default: diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9d7d970285..fc2bb3c91e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1714,9 +1714,20 @@ public final class ChatListNode: ListView { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) - controller.navigationPresentation = .modal - self.push?(controller) + if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first { + let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions) + controller.navigationPresentation = .modal + self.push?(controller) + }) + } else { + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.push?(controller) + } }, openPremiumManagement: { [weak self] in guard let self else { return diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 966d2d5a2e..12346ecdb7 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] let actionIcon: ContactsPeerItemActionIcon + let searchQuery: String? let action: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? @@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { actionIcon: ContactsPeerItemActionIcon = .none, index: SortIndex?, header: ListViewItemHeader?, + searchQuery: String? = nil, action: ((ContactsPeerItemPeer) -> Void)?, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, @@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.options = options self.additionalActions = additionalActions self.actionIcon = actionIcon + self.searchQuery = searchQuery self.action = action self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) } case let .addressName(suffix): - if let addressName = peer.addressName { + var addressName = peer.addressName + if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) { + for username in peer.usernames { + if username.username.lowercased().contains(searchQuery) { + addressName = username.username + break + } + } + } + if let addressName { let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor) if !suffix.isEmpty { let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 97c3c609a0..5ee8026860 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -462,6 +462,10 @@ public extension EnginePeer { var addressName: String? { return self._asPeer().addressName } + + var usernames: [TelegramPeerUsername] { + return self._asPeer().usernames + } var indexName: EnginePeer.IndexName { return EnginePeer.IndexName(self._asPeer().indexName) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index f6099c2e55..aa07479d98 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } } } - + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var renderedMyPeers: [FoundPeer] = [] @@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } @@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index 1d258a7b4f..5585d5c5d7 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent { switch component.subject { case .peer: if let peer = state.peer { - if case .user = peer { - mainTitle = environment.strings.Report_Title_User + if case let .user(user) = peer { + if let _ = user.botInfo { + mainTitle = environment.strings.Report_Title_Bot + } else { + mainTitle = environment.strings.Report_Title_User + } } else if case let .channel(channel) = peer, case .broadcast = channel.info { mainTitle = environment.strings.Report_Title_Channel } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 538c7ca8eb..e383df3049 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -564,8 +564,7 @@ final class GiftSetupScreenComponent: Component { contentHeight += 26.0 if case let .starGift(starGift) = component.subject, let availability = starGift.availability { - //TODO:localize - let remains: Int32 = Int32(CGFloat(availability.remains) * 0.66) + let remains: Int32 = availability.remains let position = CGFloat(remains) / CGFloat(availability.total) let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) From 1dc39230e0505f07460252e0388faf9378b1bf42 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 18:04:09 +0400 Subject: [PATCH 07/19] Various fixes --- .../GiftViewScreen/Sources/GiftViewScreen.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index d5466b4a84..a97db9618a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -185,6 +185,7 @@ private final class GiftViewSheetContent: CombinedComponent { var savedToProfile = false var converted = false var giftId: Int64 = 0 + var date: Int32 = 0 if let arguments = component.subject.arguments { animationFile = arguments.gift.file stars = arguments.gift.price @@ -196,6 +197,7 @@ private final class GiftViewSheetContent: CombinedComponent { savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = arguments.gift.id + date = arguments.date } else { animationFile = nil stars = 0 @@ -333,7 +335,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "date", title: strings.Gift_View_Date, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) @@ -342,11 +344,13 @@ private final class GiftViewSheetContent: CombinedComponent { if let gift = state.starGiftsMap[giftId], let availability = gift.availability { remains = availability.remains } + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator) tableItems.append(.init( id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remains)", "\(limitTotal)").string, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) )) } @@ -719,14 +723,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) - var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { - return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } From f174769d2d43be487564995403994ed2dcf6fe63 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 19:46:03 +0400 Subject: [PATCH 08/19] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../MinimizeKeyboardGestureRecognizer.swift | 20 ----------- .../Sources/GiftSetupScreen.swift | 27 ++++++++++----- .../Sources/GiftViewScreen.swift | 34 ++++++++++++++----- .../Sources/PeerInfoGiftsPaneNode.swift | 7 ++-- versions.json | 2 +- 6 files changed, 50 insertions(+), 41 deletions(-) delete mode 100644 submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6d7999efb1..39da024bab 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13038,6 +13038,7 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName" = "Hide My Name"; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; +"Gift.Send.Limited" = "Limited"; "Profile.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift"; diff --git a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift b/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift deleted file mode 100644 index b8c2ed10c5..0000000000 --- a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import UIKit - -final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.cancelsTouchesInView = false - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - self.delegate = self - - self.direction = [.left, .right] - self.numberOfTouchesRequired = 2 - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e383df3049..713deadceb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -143,6 +143,8 @@ final class GiftSetupScreenComponent: Component { self.addSubview(self.scrollView) self.scrollView.layer.addSublayer(self.topOverscrollLayer) + + self.disablesInteractiveKeyboardGestureRecognizer = true } required init?(coder: NSCoder) { @@ -352,6 +354,8 @@ final class GiftSetupScreenComponent: Component { } navigationController.setViewControllers(controllers, animated: true) } + + starsContext.load(force: true) }) }) } @@ -459,9 +463,7 @@ final class GiftSetupScreenComponent: Component { return } if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - if self.textInputState.isEditing { - textInputView.insertText(text: text) - } + textInputView.insertText(text: text) } }, backwardsDeleteText: { [weak self] in @@ -510,6 +512,16 @@ final class GiftSetupScreenComponent: Component { } } ) + + if case .starGift = component.subject { + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } } let environment = environment[EnvironmentType.self].value @@ -566,14 +578,14 @@ final class GiftSetupScreenComponent: Component { if case let .starGift(starGift) = component.subject, let availability = starGift.availability { let remains: Int32 = availability.remains let position = CGFloat(remains) / CGFloat(availability.total) - let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let remainsString = "\(remains)" let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], - inactiveTitle: "Limited", + inactiveTitle: environment.strings.Gift_Send_Limited, inactiveValue: "", inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, activeTitle: "", @@ -1001,7 +1013,7 @@ final class GiftSetupScreenComponent: Component { } } } - if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) { + if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode { if self.textInputState.isEditing { self.recenterOnTag = self.textInputTag } @@ -1251,9 +1263,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { fatalError("init(coder:) has not been implemented") } - deinit { - } - @objc private func cancelPressed() { self.dismiss() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index a97db9618a..ff09e4d3a4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -181,6 +181,7 @@ private final class GiftViewSheetContent: CombinedComponent { let text: String? let entities: [MessageTextEntity]? let limitTotal: Int32? + var outgoing = false var incoming = false var savedToProfile = false var converted = false @@ -193,6 +194,11 @@ private final class GiftViewSheetContent: CombinedComponent { entities = arguments.entities limitTotal = arguments.gift.availability?.total convertStars = arguments.convertStars + if case .message = component.subject { + outgoing = !arguments.incoming + } else { + outgoing = false + } incoming = arguments.incoming || arguments.peerId == component.context.account.peerId savedToProfile = arguments.savedToProfile converted = arguments.converted @@ -238,12 +244,12 @@ private final class GiftViewSheetContent: CombinedComponent { } var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) - if !incoming && stars > 0 { + if outgoing { formattedAmount = "- \(formattedAmount)" } let countFont: UIFont = Font.semibold(17.0) let amountText = formattedAmount - let countColor = incoming ? theme.list.itemDisclosureActions.constructive.fillColor : theme.list.itemDestructiveColor + let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor let title = title.update( component: MultilineTextComponent( @@ -367,7 +373,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, - text: .plain(attributedText) + text: .plain(attributedText), + maximumNumberOfLines: 0 ) ) )) @@ -1071,17 +1078,26 @@ private final class TableComponent: CombinedComponent { } else { insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) } - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append((valueChild, insets)) var titleHeight: CGFloat = 0.0 if let titleChild = updatedTitleChildren[i] { titleHeight = titleChild.size.height } + + let availableValueWidth: CGFloat + if titleHeight > 0.0 { + availableValueWidth = rightColumnWidth + } else { + availableValueWidth = context.availableSize.width + } + + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) rowHeights[i] = rowHeight totalHeight += rowHeight diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index dce8f22379..4becea5f6d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -138,8 +138,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize) + + var index: Int32 = 0 for product in starsProducts { - let itemId = AnyHashable(product.date) + let itemId = AnyHashable(index) validIds.append(itemId) var itemTransition = transition @@ -221,6 +223,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemFrame.origin.x = sideInset itemFrame.origin.y += starsOptionSize.height + optionSpacing } + index += 1 } var removeIds: [AnyHashable] = [] @@ -243,7 +246,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.starsItems.removeValue(forKey: id) } - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0 if self.peerId == self.context.account.peerId { let transition = ComponentTransition.immediate diff --git a/versions.json b/versions.json index da4ab2f6e5..3556a03d85 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.2", + "app": "11.2.1", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0" From 9e1dc11997fd97b8d15a80fee17b61e0cef60c39 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 5 Oct 2024 20:14:59 +0400 Subject: [PATCH 09/19] HLS video improvements --- .../GalleryUI/Sources/GalleryController.swift | 11 +- .../Sources/GalleryControllerNode.swift | 6 +- .../GalleryUI/Sources/GalleryItemNode.swift | 2 +- .../GalleryUI/Sources/GalleryPagerNode.swift | 2 +- .../Sources/Items/ChatImageGalleryItem.swift | 4 +- .../Items/UniversalVideoGalleryItem.swift | 331 +++++++++++++++++- .../SecretMediaPreviewController.swift | 2 +- .../InstantPageGalleryController.swift | 2 +- .../VideoChatActionButtonComponent.swift | 3 +- .../Sources/VideoChatScreen.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 24 +- .../Sources/HLSVideoContent.swift | 17 +- .../Sources/NativeVideoContent.swift | 18 +- 13 files changed, 395 insertions(+), 29 deletions(-) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 1f1e17ba77..b141da652a 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -1405,8 +1405,15 @@ public class GalleryController: ViewController, StandalonePresentableController, } } - self.galleryNode.completeCustomDismiss = { [weak self] in - self?._hiddenMedia.set(.single(nil)) + self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in + if isPictureInPicture { + if let chatController = self?.baseNavigationController?.topViewController as? ChatController { + chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0))) + } + } else { + self?._hiddenMedia.set(.single(nil)) + } + self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index c0f1930656..84cccba91c 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture public var pager: GalleryPagerNode public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } @@ -123,9 +123,9 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } } - self.pager.completeCustomDismiss = { [weak self] in + self.pager.completeCustomDismiss = { [weak self] isPictureInPicture in if let strongSelf = self { - strongSelf.completeCustomDismiss() + strongSelf.completeCustomDismiss(isPictureInPicture) } } diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 3cbd05e5a6..222a018862 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -25,7 +25,7 @@ open class GalleryItemNode: ASDisplayNode { public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } public var alternativeDismiss: () -> Bool = { return false } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 1ee1b3af55..59bfe42de1 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -110,7 +110,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } - public var completeCustomDismiss: () -> Void = { } + public var completeCustomDismiss: (Bool) -> Void = { _ in } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index bb911f25ea..3085831148 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -687,7 +687,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + self.completeCustomDismiss(false) } } f(.default) @@ -719,7 +719,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + self.completeCustomDismiss(false) } } f(.default) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 9f95f5e933..a04cc80bc0 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -804,6 +804,245 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte } } +@available(iOS 15.0, *) +private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate { + private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { + private let node: UniversalVideoNode + private var statusDisposable: Disposable? + private var status: MediaPlayerStatus? + weak var pictureInPictureController: AVPictureInPictureController? + + private var previousIsPlaying = false + init(node: UniversalVideoNode) { + self.node = node + + super.init() + + var invalidatedStateOnce = false + self.statusDisposable = (self.node.status + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + strongSelf.status = status + if let status { + let isPlaying = status.status == .playing + if !invalidatedStateOnce { + invalidatedStateOnce = true + strongSelf.pictureInPictureController?.invalidatePlaybackState() + } else if strongSelf.previousIsPlaying != isPlaying { + strongSelf.previousIsPlaying = isPlaying + strongSelf.pictureInPictureController?.invalidatePlaybackState() + } + } + }).strict() + } + + deinit { + self.statusDisposable?.dispose() + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { + self.node.togglePlayPause() + } + + public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { + guard let status = self.status else { + return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0))) + } + return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration - status.timestamp, preferredTimescale: CMTimeScale(30.0))) + } + + public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + guard let status = self.status else { + return false + } + switch status.status { + case .playing: + return false + case .buffering, .paused: + return true + } + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { + let node = self.node + let _ = (self.node.status + |> take(1) + |> deliverOnMainQueue).start(next: { [weak node] status in + if let node = node, let timestamp = status?.timestamp, let duration = status?.duration { + let nextTimestamp = timestamp + skipInterval.seconds + if nextTimestamp > duration { + node.seek(0.0) + node.pause() + } else { + node.seek(min(duration, nextTimestamp)) + } + } + + completionHandler() + }) + } + + public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + } + + private let context: AccountContext + private let accountId: AccountRecordId + private let hiddenMedia: (MessageId, Media)? + private weak var mediaManager: MediaManager? + private var pictureInPictureController: AVPictureInPictureController? + private var contentDelegate: PlaybackDelegate? + private let node: UniversalVideoNode + private let willBegin: (NativePictureInPictureContentImpl) -> Void + private let didBegin: (NativePictureInPictureContentImpl) -> Void + private let expand: (@escaping () -> Void) -> Void + private var pictureInPictureTimer: SwiftSignalKit.Timer? + private var didExpand: Bool = false + + private var hiddenMediaManagerIndex: Int? + + private var messageRemovedDisposable: Disposable? + + private var isNativePictureInPictureActiveDisposable: Disposable? + + init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { + self.context = context + self.mediaManager = mediaManager + self.accountId = accountId + self.hiddenMedia = hiddenMedia + self.node = videoNode + self.willBegin = willBegin + self.didBegin = didBegin + self.expand = expand + + super.init() + + if let videoLayer = videoNode.getVideoLayer() { + let contentDelegate = PlaybackDelegate(node: self.node) + self.contentDelegate = contentDelegate + + let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate)) + self.pictureInPictureController = pictureInPictureController + contentDelegate.pictureInPictureController = pictureInPictureController + + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + pictureInPictureController.requiresLinearPlayback = !canSkip + pictureInPictureController.delegate = self + self.pictureInPictureController = pictureInPictureController + } + + if let (messageId, _) = hiddenMedia { + self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> map { message -> Bool in + if let _ = message { + return false + } else { + return true + } + } + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.node.canAttachContent = false + }) + } + } + + deinit { + self.messageRemovedDisposable?.dispose() + self.isNativePictureInPictureActiveDisposable?.dispose() + self.pictureInPictureTimer?.invalidate() + self.node.setCanPlaybackWithoutHierarchy(false) + + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager { + mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + } + } + + func updateIsCentral(isCentral: Bool) { + guard let pictureInPictureController = self.pictureInPictureController else { + return + } + + if isCentral { + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true + } else { + pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false + } + } + + func beginPictureInPicture() { + guard let pictureInPictureController = self.pictureInPictureController else { + return + } + if pictureInPictureController.isPictureInPicturePossible { + pictureInPictureController.startPictureInPicture() + } + } + + func invalidatePlaybackState() { + self.pictureInPictureController?.invalidatePlaybackState() + } + + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.node.setCanPlaybackWithoutHierarchy(true) + + if let hiddenMedia = self.hiddenMedia, let mediaManager = self.mediaManager { + let accountId = self.accountId + self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia) + |> map { messageIdAndMedia in + if let (messageId, media) = messageIdAndMedia { + return .chat(accountId, messageId, media) + } else { + return nil + } + }) + } + + self.willBegin(self) + } + + public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.didBegin(self) + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { + print(error) + } + + public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + } + + public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + self.node.setCanPlaybackWithoutHierarchy(false) + if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager { + mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) + self.hiddenMediaManagerIndex = nil + } + } + + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + self.expand { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.didExpand = true + + completionHandler(true) + } + } +} + final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private let presentationData: PresentationData @@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) private let hasExpandedCaptionPromise = Promise() private var hideControlsDisposable: Disposable? + private var automaticPictureInPictureDisposable: Disposable? var playbackCompleted: (() -> Void)? private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? private var pictureInPictureContent: AnyObject? + private var nativePictureInPictureContent: AnyObject? + + private var activePictureInPictureNavigationController: NavigationController? + private var activePictureInPictureController: ViewController? init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context @@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() + self.automaticPictureInPictureDisposable?.dispose() } override func ready() -> Signal { @@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.videoNode?.setBaseRate(playbackRate) } } + + if strongSelf.nativePictureInPictureContent == nil { + strongSelf.setupNativePictureInPicture() + } } } self.videoNode = videoNode @@ -1749,6 +1998,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } + + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.updateIsCentral(isCentral: isCentral) + } + } } } @@ -2330,13 +2585,79 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.beginCustomDismiss(false) self.statusNode.isHidden = true self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() + self?.completeCustomDismiss(false) }) } } } + private func setupNativePictureInPicture() { + guard let item = self.item, let videoNode = self.videoNode else { + return + } + + var hiddenMedia: (MessageId, Media)? = nil + switch item.contentInfo { + case let .message(message, _): + for media in message.media { + if let media = media as? TelegramMediaImage { + hiddenMedia = (message.id, media) + } else if let media = media as? TelegramMediaFile, media.isVideo { + hiddenMedia = (message.id, media) + } + } + default: + break + } + + if #available(iOS 15.0, *) { + let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in + guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else { + return + } + self.activePictureInPictureNavigationController = navigationController + self.activePictureInPictureController = controller + + controller.view.alpha = 0.0 + controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.completeCustomDismiss(true) + }) + }, didBegin: { [weak self] _ in + guard let self else { + return + } + let _ = self + }, expand: { [weak self] completion in + guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else { + completion() + return + } + + self.activePictureInPictureController = nil + self.activePictureInPictureNavigationController = nil + + activePictureInPictureController.presentationArguments = nil + activePictureInPictureNavigationController.currentWindow?.present(activePictureInPictureController, on: .root, blockInteraction: false, completion: { + }) + + activePictureInPictureController.view.alpha = 1.0 + activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in + completion() + }) + }) + + self.nativePictureInPictureContent = content + } + } + @objc func pictureInPictureButtonPressed() { + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.beginPictureInPicture() + return + } + } + var isNativePictureInPictureSupported = false switch self.item?.contentInfo { case let .message(message, _): @@ -2391,7 +2712,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let strongSelf = self else { return } - strongSelf.completeCustomDismiss() + strongSelf.completeCustomDismiss(false) }, expand: { [weak baseNavigationController] completion in guard let contentInfo = item.contentInfo else { return @@ -2524,7 +2845,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.beginCustomDismiss(false) self.statusNode.isHidden = true self.animateOut(toOverlay: overlayNode, completion: { [weak self] in - self?.completeCustomDismiss() + self?.completeCustomDismiss(false) }) } } @@ -2847,7 +3168,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false))) Queue.mainQueue().after(0.3) { - strongSelf.completeCustomDismiss() + strongSelf.completeCustomDismiss(false) } } f(.default) @@ -2928,7 +3249,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true))) Queue.mainQueue().after(0.3) { - self.completeCustomDismiss() + self.completeCustomDismiss(false) } } f(.default) diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index 0360571fe4..2d5c388ef5 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -300,7 +300,7 @@ public final class SecretMediaPreviewController: ViewController { } } - self.controllerNode.completeCustomDismiss = { [weak self] in + self.controllerNode.completeCustomDismiss = { [weak self] _ in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index 2a3040ef38..3735bdd4b8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -398,7 +398,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable self?.presentingViewController?.dismiss(animated: false, completion: nil) } - self.galleryNode.completeCustomDismiss = { [weak self] in + self.galleryNode.completeCustomDismiss = { [weak self] _ in self?._hiddenMedia.set(.single(nil)) self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 656494c344..f89a6bda96 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -160,7 +160,7 @@ final class VideoChatActionButtonComponent: Component { case .unmuted: backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) case .raiseHand, .scheduled: - backgroundColor = UIColor(rgb: 0x3252EF) + backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 case let .video(isActive): @@ -282,6 +282,7 @@ final class VideoChatActionButtonComponent: Component { } self.isEnabled = isEnabled + self.isUserInteractionEnabled = isEnabled return size } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index 35c8beec52..a6a0564e8c 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1022,7 +1022,7 @@ final class VideoChatScreenComponent: Component { if case let .channel(channel) = self.peer, case .broadcast = channel.info { displayEvent = false } - if members.totalCount < 250 { + if members.totalCount < 40 { displayEvent = true } else if event.peer.isVerified { displayEvent = true diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 8a36726273..ed93377cda 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + let effectiveMediaVisibility = self.visibility + var isPlaying = true - if case let .visible(_, subRect) = self.visibility { - if subRect.minY > 32.0 { - isPlaying = false - } - } else { - isPlaying = false - } if !item.controllerInteraction.canReadHistory { isPlaying = false } + if self.forceStopAnimations { isPlaying = false } @@ -6228,7 +6224,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + if contentNode is ChatMessageMediaBubbleContentNode { + contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + } else { + contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) + } + } + + if case let .visible(_, subRect) = self.visibility { + if subRect.minY > 32.0 { + isPlaying = false + } + } else { + isPlaying = false } if let threadInfoNode = self.threadInfoNode { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index 77b46d4398..de44e0b2c6 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -25,9 +25,22 @@ public final class HLSQualitySet { if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { for attribute in alternativeFile.attributes { if case let .Video(_, size, _, _, _, videoCodec) = attribute { - let _ = size if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { - qualityFiles[Int(size.height)] = baseFile.withMedia(alternativeFile) + let key = Int(size.height) + if let currentFile = qualityFiles[key] { + var currentCodec: String? + for attribute in currentFile.media.attributes { + if case let .Video(_, _, _, _, _, videoCodec) = attribute { + currentCodec = videoCodec + } + } + if let currentCodec, currentCodec == "av1" { + } else { + qualityFiles[key] = baseFile.withMedia(alternativeFile) + } + } else { + qualityFiles[key] = baseFile.withMedia(alternativeFile) + } } } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index 07909d446f..f8e38da318 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -11,6 +11,7 @@ import AccountContext import PhotoResources import UIKitRuntimeUtils import RangeSet +import VideoToolbox private extension CGRect { var center: CGPoint { @@ -25,6 +26,11 @@ public enum NativeVideoContentId: Hashable { case profileVideo(Int64, String?) } +private let isAv1Supported: Bool = { + let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) + return value +}() + public final class NativeVideoContent: UniversalVideoContent { public let id: AnyHashable public let nativeId: NativeVideoContentId @@ -58,7 +64,17 @@ public final class NativeVideoContent: UniversalVideoContent { let hasSentFramesToDisplay: (() -> Void)? public static func isVideoCodecSupported(videoCodec: String) -> Bool { - return videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" + if videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" { + return true + } + + if videoCodec == "av1" { + if isAv1Supported { + return true + } + } + + return false } public static func isHLSVideo(file: TelegramMediaFile) -> Bool { From a6dffb4e3e65ee2d021c31c10d6e13f9d4480b51 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 5 Oct 2024 20:18:25 +0400 Subject: [PATCH 10/19] Video chat improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 ++ .../VideoChatActionButtonComponent.swift | 4 ++-- .../Sources/VideoChatMicButtonComponent.swift | 23 +++++++++++-------- .../VideoChatParticipantsComponent.swift | 14 +++++------ .../VideoChatScheduledInfoComponent.swift | 2 +- .../Sources/VideoChatScreen.swift | 5 ++-- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 68fc11e9fb..3f5035a237 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13051,3 +13051,5 @@ Sorry for the inconvenience."; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; "Report.Send" = "Send Report"; + +f diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index f89a6bda96..beef446dc2 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -164,7 +164,7 @@ final class VideoChatActionButtonComponent: Component { } iconDiameter = 60.0 case let .video(isActive): - titleText = "video" + titleText = component.strings.VoiceChat_Video switch component.microphoneState { case .connecting: backgroundColor = UIColor(white: 0.1, alpha: 1.0) @@ -177,7 +177,7 @@ final class VideoChatActionButtonComponent: Component { } iconDiameter = 60.0 case .leave: - titleText = "leave" + titleText = component.strings.VoiceChat_Leave backgroundColor = UIColor(rgb: 0x47191E) iconDiameter = 22.0 } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift index d7db6cc15f..1541bf5ff9 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMicButtonComponent.swift @@ -189,6 +189,7 @@ final class VideoChatMicButtonComponent: Component { } let call: PresentationGroupCall + let strings: PresentationStrings let content: Content let isCollapsed: Bool let updateUnmutedStateIsPushToTalk: (Bool?) -> Void @@ -197,6 +198,7 @@ final class VideoChatMicButtonComponent: Component { init( call: PresentationGroupCall, + strings: PresentationStrings, content: Content, isCollapsed: Bool, updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void, @@ -204,6 +206,7 @@ final class VideoChatMicButtonComponent: Component { scheduleAction: @escaping () -> Void ) { self.call = call + self.strings = strings self.content = content self.isCollapsed = isCollapsed self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk @@ -327,29 +330,29 @@ final class VideoChatMicButtonComponent: Component { var isEnabled = true switch component.content { case .connecting: - titleText = "Connecting..." + titleText = component.strings.VoiceChat_Connecting isEnabled = false case .muted: - titleText = "Unmute" + titleText = component.strings.VoiceChat_Unmute case let .unmuted(isPushToTalk): - titleText = isPushToTalk ? "You are Live" : "Tap to Mute" + titleText = isPushToTalk ? component.strings.VoiceChat_Live : component.strings.VoiceChat_Mute case let .raiseHand(isRaised): if isRaised { - titleText = "You asked to speak" - subtitleText = "We let the speakers know" + titleText = component.strings.VoiceChat_AskedToSpeak + subtitleText = component.strings.VoiceChat_AskedToSpeakHelp } else { - titleText = "Muted by Admin" - subtitleText = "Tap if you want to speak" + titleText = component.strings.VoiceChat_MutedByAdmin + subtitleText = component.strings.VoiceChat_MutedByAdminHelp } case let .scheduled(state): switch state { case .start: - titleText = "Start Now" + titleText = component.strings.VoiceChat_StartNow case let .toggleSubscription(isSubscribed): if isSubscribed { - titleText = "Clear Reminder" + titleText = component.strings.VoiceChat_CancelReminder } else { - titleText = "Set Reminder" + titleText = component.strings.VoiceChat_SetReminder } } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 7b98a0b136..5d8605d1ef 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1193,17 +1193,17 @@ final class VideoChatParticipantsComponent: Component { let subtitle: PeerListItemComponent.Subtitle if participant.peer.id == component.call.accountContext.account.peerId { - subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent) } else if component.speakingParticipants.contains(participant.peer.id) { if let volume = participant.volume, volume / 100 != 100 { - subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeakingVolume("\(volume / 100)%").string, color: .constructive) } else { - subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeaking, color: .constructive) } } else if let about = participant.about, !about.isEmpty { subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral) } else { - subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral) + subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral) } let rightAccessoryComponent: AnyComponent = AnyComponent(VideoChatParticipantStatusComponent( @@ -1667,12 +1667,12 @@ final class VideoChatParticipantsComponent: Component { if let participants = component.participants, let inviteType = participants.inviteType { switch inviteType { case .invite: - inviteText = "Invite Members" + inviteText = component.strings.VoiceChat_InviteMember case .shareLink: - inviteText = "Share Invite Link" + inviteText = component.strings.VoiceChat_Share } } else { - inviteText = "Invite Members" + inviteText = component.strings.VoiceChat_InviteMember } let inviteListItemSize = self.inviteListItemView.update( transition: transition, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift index 3c97c76306..c9ae7d1dea 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift @@ -123,7 +123,7 @@ final class VideoChatScheduledInfoComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Starts in", font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) + text: .plain(NSAttributedString(string: component.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white)) )), environment: {}, containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index a6a0564e8c..f49f82e495 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1270,9 +1270,9 @@ final class VideoChatScreenComponent: Component { if callState.networkState == .connected, let members = self.members { idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) } else if callState.scheduleTimestamp != nil { - idleTitleStatusText = "scheduled" + idleTitleStatusText = environment.strings.VoiceChat_Scheduled } else { - idleTitleStatusText = "connecting..." + idleTitleStatusText = environment.strings.VoiceChat_Connecting } } else { idleTitleStatusText = " " @@ -1686,6 +1686,7 @@ final class VideoChatScreenComponent: Component { transition: transition, component: AnyComponent(VideoChatMicButtonComponent( call: component.call, + strings: environment.strings, content: micButtonContent, isCollapsed: areButtonsCollapsed, updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in From bf21a499b77823943c3ea1f31a64ba854c85b44f Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 5 Oct 2024 20:19:40 +0400 Subject: [PATCH 11/19] Add killswitch --- .../Sources/Items/UniversalVideoGalleryItem.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index a04cc80bc0..09df0e7fc8 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2596,6 +2596,18 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } + if !(item.content is NativeVideoContent) { + return + } + + var useNative = true + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip"] { + useNative = false + } + if !useNative { + return + } + var hiddenMedia: (MessageId, Media)? = nil switch item.contentInfo { case let .message(message, _): From eb4519e192a907401131aef833623307994062dd Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 02:03:53 +0400 Subject: [PATCH 12/19] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 7 ++ .../AvatarNode/Sources/AvatarNode.swift | 4 +- .../Sources/BotCheckoutControllerNode.swift | 24 +++--- .../Payments/BotPaymentForm.swift | 3 + .../Sources/ChatMessageBubbleItemNode.swift | 2 +- .../ChatMessageGiftBubbleContentNode.swift | 79 +++++++++++++------ .../Sources/GiftItemComponent.swift | 24 +++++- .../Sources/GiftOptionsScreen.swift | 47 ++++++++--- .../Components/Gifts/GiftSetupScreen/BUILD | 1 + .../Sources/ChatGiftPreviewItem.swift | 2 +- .../Sources/GiftSetupScreen.swift | 62 ++++++++++++++- .../Sources/RemainingCountComponent.swift | 22 +----- 12 files changed, 204 insertions(+), 73 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 39da024bab..2a1e6d8e10 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13008,6 +13008,8 @@ Sorry for the inconvenience."; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.Limited" = "Limited"; "Gift.Options.Gift.Limited" = "Limited"; +"Gift.Options.Gift.SoldOut" = "Sold Out"; +"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out."; "PeerInfo.PaneGifts" = "Gifts"; @@ -13039,6 +13041,10 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Limited" = "Limited"; +"Gift.Send.Left" = "left"; + +"Gift.Send.ErrorUnknown" = "An error occurred. Please try again."; +"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift."; "Profile.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift"; @@ -13061,6 +13067,7 @@ Sorry for the inconvenience."; "Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; "Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; +"Notification.PremiumGift.More" = "more"; "Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 3fcfba0dd7..d4870d516a 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -931,13 +931,13 @@ public final class AvatarNode: ASDisplayNode { if let repliesIcon = repliesIcon { context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) } - } else if case .anonymousSavedMessagesIcon = parameters.icon { + } else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon { let factor = bounds.size.width / 60.0 context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: factor, y: -factor) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) - if let theme = parameters.theme, theme.overallDarkAppearance { + if let theme = parameters.theme, theme.overallDarkAppearance, !isColored { if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon { context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size)) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 6392b0de64..cbf8994a0f 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1558,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } - let text: String + let text: String? switch error { - case .precheckoutFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed - case .paymentFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed - case .alreadyPaid: - text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid - case .generic: - text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + case .precheckoutFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed + case .paymentFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed + case .alreadyPaid: + text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid + case .generic: + text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + default: + text = nil } - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + if let text { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) + } strongSelf.failed() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 1296173518..a00e0e0c84 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -588,6 +588,7 @@ public enum SendBotPaymentFormError { case precheckoutFailed case paymentFailed case alreadyPaid + case starGiftOutOfStock } public enum SendBotPaymentResult { @@ -702,6 +703,8 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa return .fail(.paymentFailed) } else if error.errorDescription == "INVOICE_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { + return .fail(.starGiftOutOfStock) } return .fail(.generic) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ed93377cda..601a067d4b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -6224,7 +6224,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - if contentNode is ChatMessageMediaBubbleContentNode { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode { contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } else { contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index eecab9955f..41b478eb1e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -50,6 +50,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode + private let moreTextNode: TextNode + private var maskView: UIImageView? private var maskOverlayView: UIView? @@ -61,6 +63,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var isExpanded: Bool = false private var appliedIsExpanded: Bool = false + private var isStarGift = false + private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { @@ -138,6 +142,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.ribbonTextNode.isUserInteractionEnabled = false self.ribbonTextNode.displaysAsynchronously = false + self.moreTextNode = TextNode() + self.moreTextNode.isUserInteractionEnabled = false + self.moreTextNode.displaysAsynchronously = false + super.init() self.addSubnode(self.labelNode) @@ -147,6 +155,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.textClippingNode.addSubnode(self.subtitleNode.textNode) self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) + self.addSubnode(self.moreTextNode) self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) @@ -183,6 +192,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.currentProgressDisposable?.dispose() } + override public func didLoad() { + super.didLoad() + + self.maskView = UIImageView() + + let maskOverlayView = UIView() + maskOverlayView.alpha = 0.0 + maskOverlayView.backgroundColor = .white + self.maskOverlayView = maskOverlayView + + self.maskView?.addSubview(maskOverlayView) + } + @objc private func buttonPressed() { guard let item = self.item else { return @@ -190,6 +212,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress())) } + private func expandPressed() { + self.isExpanded = !self.isExpanded + guard let item = self.item else{ + return + } + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } + private func makeProgress() -> Promise { let progress = Promise() self.currentProgressDisposable?.dispose() @@ -263,6 +293,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode) let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage @@ -290,6 +321,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var ribbonTitle = "" var hasServiceMessage = true var textSpacing: CGFloat = 0.0 + var isStarGift = false for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { @@ -377,6 +409,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { hasServiceMessage = false } case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): + isStarGift = true let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = item.presentationData.strings.Notification_StarGift_Title(authorName).string if let giftText, !giftText.isEmpty { @@ -439,8 +472,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let attributedText: NSAttributedString - if let _ = animationFile { + if !entities.isEmpty { attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil) } else { attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( @@ -578,6 +613,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.item = item + strongSelf.isStarGift = isStarGift strongSelf.updateVisibility() @@ -599,6 +635,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { )) let _ = buttonTitleApply() let _ = ribbonTextApply() + let _ = moreApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -606,7 +643,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight)) + let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) @@ -617,10 +654,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) } if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { - animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) - animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) + animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil) } - + animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil) if !subtitleLayout.spoilers.isEmpty { let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -734,24 +771,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateAbsoluteRect(rect, within: size) } - if canExpand { - if strongSelf.maskView?.image == nil { - strongSelf.maskView?.image = generateMaskImage() + if canExpand, let maskView = strongSelf.maskView { + if maskView.image == nil { + maskView.image = generateMaskImage() } strongSelf.textClippingNode.view.mask = strongSelf.maskView -// var expandIconFrame: CGRect = .zero -// if let icon = strongSelf.expandIcon.image { -// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size) -// if wasHidden || isFirstTime { -// strongSelf.expandIcon.position = expandIconFrame.center -// } else { -// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) -// } -// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) -// } + animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil) } else { strongSelf.textClippingNode.view.mask = nil + strongSelf.moreTextNode.alpha = 0.0 } switch strongSelf.visibility { @@ -876,6 +905,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) + } else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero { + self.expandPressed() + return ChatMessageBubbleContentTapAction(content: .ignore) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundContent?.frame.contains(point) == true { @@ -934,7 +966,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !alreadySeen && self.animationNode.isPlaying { item.controllerInteraction.playNextOutgoingGift = false - Queue.mainQueue().after(1.0) { + + Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) { item.controllerInteraction.animateDiceSuccess(false, true) } } @@ -943,7 +976,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } private func generateMaskImage() -> UIImage? { - return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in + return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) @@ -956,7 +989,7 @@ private func generateMaskImage() -> UIImage? { let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.setBlendMode(.copy) - context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) - context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) - })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0)) + context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0))) + context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 2f1b3612be..fda1f35734 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -64,6 +64,7 @@ public final class GiftItemComponent: Component { let ribbon: Ribbon? let isLoading: Bool let isHidden: Bool + let isSoldOut: Bool public init( context: AccountContext, @@ -75,7 +76,8 @@ public final class GiftItemComponent: Component { price: String, ribbon: Ribbon? = nil, isLoading: Bool = false, - isHidden: Bool = false + isHidden: Bool = false, + isSoldOut: Bool = false ) { self.context = context self.theme = theme @@ -87,6 +89,7 @@ public final class GiftItemComponent: Component { self.ribbon = ribbon self.isLoading = isLoading self.isHidden = isHidden + self.isSoldOut = isSoldOut } public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { @@ -120,6 +123,9 @@ public final class GiftItemComponent: Component { if lhs.isHidden != rhs.isHidden { return false } + if lhs.isSoldOut != rhs.isSoldOut { + return false + } return true } @@ -284,14 +290,26 @@ public final class GiftItemComponent: Component { } } + let buttonColor: UIColor + var isStars = false + if component.isSoldOut { + buttonColor = component.theme.list.itemDestructiveColor + } else if component.price.containsEmoji { + buttonColor = UIColor(rgb: 0xd3720a) + isStars = true + } else { + buttonColor = component.theme.list.itemAccentColor + } + let buttonSize = self.button.update( transition: transition, component: AnyComponent( ButtonContentComponent( context: component.context, text: component.price, - color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor, - isStars: component.price.containsEmoji) + color: buttonColor, + isStars: isStars + ) ), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index f7c9b5873b..4171c761bc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import InAppPurchaseManager import TabSelectorComponent import GiftSetupScreen +import UndoUI final class GiftOptionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -173,6 +174,20 @@ final class GiftOptionsScreenComponent: Component { self.updateScrolling(transition: .immediate) } + private func dismissAllTooltips(controller: ViewController) { + controller.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + return true + }) + controller.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitAction() + } + }) + } + private func updateScrolling(transition: ComponentTransition) { guard let environment = self.environment, let component = self.component else { return @@ -274,6 +289,7 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } + let isSoldOut = gift.availability?.remains == 0 let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -284,13 +300,14 @@ final class GiftOptionsScreenComponent: Component { theme: environment.theme, peer: nil, subject: .starGift(gift.id, gift.file), - price: "⭐️ \(gift.price)", + price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", ribbon: gift.availability != nil ? GiftItemComponent.Ribbon( text: environment.strings.Gift_Options_Gift_Limited, color: .blue ) - : nil + : nil, + isSoldOut: isSoldOut ) ), effectAlignment: .center, @@ -303,13 +320,25 @@ final class GiftOptionsScreenComponent: Component { } else { mainController = controller } - let giftController = GiftSetupScreen( - context: component.context, - peerId: component.peerId, - subject: .starGift(gift), - completion: component.completion - ) - mainController.push(giftController) + if gift.availability?.remains == 0 { + self.dismissAllTooltips(controller: mainController) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil), + elevatedLayout: false, + action: { _ in return true } + ) + mainController.present(resultController, in: .window(.root)) + } else { + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .starGift(gift), + completion: component.completion + ) + mainController.push(giftController) + } } } }, diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index a5a8867320..82507f195a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/InAppPurchaseManager", + "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 12a43a1017..4ec4cfbe93 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -258,7 +258,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { apply().1(ListViewItemApply(isOnScreen: true)) }) itemNode!.isUserInteractionEnabled = false - itemNode?.visibility = .visible(1.0, .infinite) + itemNode!.visibility = .visible(1.0, .infinite) messageNodes.append(itemNode!) self.initialBubbleHeight = itemNode?.frame.height diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 713deadceb..a91459fc09 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -29,6 +29,7 @@ import ChatPresentationInterfaceState import AudioToolbox import TextFormat import InAppPurchaseManager +import BlurredBackgroundComponent final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component { private let introContent = ComponentView() private let introSection = ComponentView() private let hideSection = ComponentView() + + private let buttonBackground = ComponentView() + private let buttonSeparator = SimpleLayer() private let button = ComponentView() private var ignoreScrolling: Bool = false @@ -356,6 +360,27 @@ final class GiftSetupScreenComponent: Component { } starsContext.load(force: true) + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + + self.inProgress = false + self.state?.updated() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var errorText: String? + switch error { + case .starGiftOutOfStock: + errorText = presentationData.strings.Gift_Send_ErrorOutOfStock + default: + errorText = presentationData.strings.Gift_Send_ErrorUnknown + } + + if let errorText = errorText { + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } }) }) } @@ -826,6 +851,34 @@ final class GiftSetupScreenComponent: Component { self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) } + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset + + let bottomPanelAlpha: CGFloat = 1.0 + let bottomPanelSize = self.buttonBackground.update( + transition: transition, + component: AnyComponent(BlurredBackgroundComponent( + color: environment.theme.rootController.tabBar.backgroundColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) + ) + self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor + self.buttonSeparator.opacity = Float(bottomPanelAlpha) + + if let view = self.buttonBackground.view { + if view.superview == nil { + self.addSubview(view) + self.layer.addSublayer(self.buttonSeparator) + } + view.alpha = bottomPanelAlpha + view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize) + self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + } + + var buttonIsEnabled = true let buttonString: String switch component.subject { case let .premium(product): @@ -834,6 +887,9 @@ final class GiftSetupScreenComponent: Component { case let .starGift(starGift): let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + if let availability = starGift.availability, availability.remains == 0 { + buttonIsEnabled = false + } } let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) @@ -856,20 +912,20 @@ final class GiftSetupScreenComponent: Component { id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), - isEnabled: true, + isEnabled: buttonIsEnabled, displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) ) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) } - buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize) + buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) } if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index 267382cf3e..bb1ab4199c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -119,6 +119,7 @@ public class RemainingCountComponent: Component { private let badgeForeground: SimpleLayer private let badgeLabel: BadgeLabelView + private let badgeLeftLabel = ComponentView() private let badgeLabelMaskView = UIImageView() private var badgeTailPosition: CGFloat = 0.0 @@ -737,7 +738,6 @@ final class BadgeLabelView: UIView { } private var itemViews: [Int: StackView] = [:] - private var staticLabel = UILabel() init() { super.init(frame: .zero) @@ -752,7 +752,6 @@ final class BadgeLabelView: UIView { var color: UIColor = .white { didSet { - self.staticLabel.textColor = self.color for (_, view) in self.itemViews { view.color = self.color } @@ -760,25 +759,6 @@ final class BadgeLabelView: UIView { } func update(value: String, transition: ComponentTransition) -> CGSize { - if value.contains(" ") { - for (_, view) in self.itemViews { - view.isHidden = true - } - - if self.staticLabel.superview == nil { - self.staticLabel.textColor = self.color - self.staticLabel.font = font - - self.addSubview(self.staticLabel) - } - - self.staticLabel.text = value - let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) - self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) - - return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) - } - let string = value let stringArray = Array(string.map { String($0) }.reversed()) From aa23e95384dbaf84b0a05302bd543048cb438a5e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 04:49:00 +0400 Subject: [PATCH 13/19] Various fixes --- .../Sources/SheetComponent.swift | 1 + .../Sources/ButtonComponent.swift | 10 +- .../ChatMessageGiftBubbleContentNode.swift | 32 ++- .../Sources/GiftOptionsScreen.swift | 5 +- .../Sources/GiftSetupScreen.swift | 8 +- .../Sources/GiftViewScreen.swift | 1 - .../Sources/PeerInfoGiftsPaneNode.swift | 4 +- .../Sources/ArchiveInfoScreen.swift | 10 +- .../Components/Stars/StarsIntroScreen/BUILD | 2 - .../Sources/StarsIntroScreen.swift | 203 +++++++----------- .../Sources/StarsTransferScreen.swift | 2 +- .../Sources/StarsWithdrawalScreen.swift | 12 +- 12 files changed, 117 insertions(+), 173 deletions(-) diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 3912465594..7e61cbe1d5 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -366,6 +366,7 @@ public final class SheetComponent: Component { self.scrollView.addSubview(contentView) } contentView.clipsToBounds = component.clipsContent + contentView.layer.cornerRadius = self.backgroundView.layer.cornerRadius if sheetEnvironment.isCentered { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index f3c401fcbb..8f4595c8b9 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -80,7 +80,7 @@ public final class ButtonBadgeComponent: Component { if contentView.superview == nil { self.addSubview(contentView) } - transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) } if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height { @@ -264,7 +264,7 @@ public final class ButtonTextContentComponent: Component { size.height = max(size.height, badgeSize.height) } - let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - measurementSize.width) * 0.5), y: floor((size.height - measurementSize.height) * 0.5)), size: measurementSize) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize) if let contentView = self.content.view { if contentView.superview == nil { @@ -274,7 +274,7 @@ public final class ButtonTextContentComponent: Component { } if let badgeSize, let badge = self.badge { - let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) + let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) if let badgeView = badge.view { var animateIn = false @@ -490,7 +490,7 @@ public final class ButtonComponent: Component { contentView.isUserInteractionEnabled = false self.addSubview(contentView) } - let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: floor((availableSize.height - contentSize.height) * 0.5)), size: contentSize) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize) contentTransition.setFrame(view: contentView, frame: contentFrame) contentTransition.setAlpha(view: contentView, alpha: contentAlpha) @@ -528,7 +528,7 @@ public final class ButtonComponent: Component { } let indicatorSize = CGSize(width: 22.0, height: 22.0) transition.setAlpha(view: activityIndicator.view, alpha: 1.0) - activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - indicatorSize.width) / 2.0), y: floor((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) } else { if let activityIndicator = self.activityIndicator { self.activityIndicator = nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 41b478eb1e..becb0dffaf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -159,7 +159,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.buttonStarsNode) - self.addSubnode(self.buttonTitleNode) + self.buttonNode.addSubnode(self.buttonTitleNode) self.addSubnode(self.ribbonBackgroundNode) self.addSubnode(self.ribbonTextNode) @@ -169,13 +169,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if highlighted { strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.alpha = 0.4 - strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonTitleNode.alpha = 0.4 } else { strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.buttonTitleNode.alpha = 1.0 - strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } @@ -544,7 +540,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { backgroundMaskImage = nil } - var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) + var backgroundSize = giftSize if hasServiceMessage { backgroundSize.height += labelLayout.size.height + 18.0 } else { @@ -556,13 +552,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let strongSelf = self { let isFirstTime = strongSelf.item == nil - var isExpandedUpdated = false if strongSelf.appliedIsExpanded != currentIsExpanded { strongSelf.appliedIsExpanded = currentIsExpanded info?.setInvertOffsetDirection() - isExpandedUpdated = true + + if let maskOverlayView = strongSelf.maskOverlayView { + animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0) + } } - let _ = isExpandedUpdated let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) @@ -637,7 +634,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = ribbonTextApply() let _ = moreApply() - let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) @@ -678,11 +675,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = nil } - let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size) - strongSelf.buttonTitleNode.frame = buttonTitleFrame - let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) - strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize) + strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - buttonTitleLayout.size.width) / 2.0), y: 8.0), size: buttonTitleLayout.size) + + animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize), completion: nil) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) if ribbonTextLayout.size.width > 0.0 { @@ -712,6 +708,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if ribbonTextLayout.size.width > 0.0 { let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0) backgroundContent.frame = backgroundMaskFrame + animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil) backgroundContent.cornerRadius = 0.0 if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size { @@ -731,7 +728,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size) } else { - backgroundContent.frame = mediaBackgroundFrame + animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) backgroundContent.clipsToBounds = true backgroundContent.cornerRadius = 24.0 backgroundContent.view.mask = nil @@ -906,8 +903,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) } else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero { - self.expandPressed() - return ChatMessageBubbleContentTapAction(content: .ignore) + return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in + self?.expandPressed() + })) } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundContent?.frame.contains(point) == true { diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 4171c761bc..869d9a61c0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -330,6 +330,7 @@ final class GiftOptionsScreenComponent: Component { action: { _ in return true } ) mainController.present(resultController, in: .window(.root)) + HapticFeedback().error() } else { let giftController = GiftSetupScreen( context: component.context, @@ -800,9 +801,7 @@ final class GiftOptionsScreenComponent: Component { guard let self, let component = self.component, let environment = self.environment else { return } - let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) - introController.navigationPresentation = .modal - + let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) if let controller = environment.controller() as? GiftOptionsScreen { let mainController: ViewController if let parentController = controller.parentController() { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index a91459fc09..510cf6b4dd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -196,6 +196,11 @@ final class GiftSetupScreenComponent: Component { if let navigationTitleView = self.navigationTitle.view { transition.setAlpha(view: navigationTitleView, alpha: 1.0) } + + let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + self.buttonBackground.view?.alpha = bottomPanelAlpha + self.buttonSeparator.opacity = Float(bottomPanelAlpha) } func proceed() { @@ -856,7 +861,6 @@ final class GiftSetupScreenComponent: Component { let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - let bottomPanelAlpha: CGFloat = 1.0 let bottomPanelSize = self.buttonBackground.update( transition: transition, component: AnyComponent(BlurredBackgroundComponent( @@ -866,14 +870,12 @@ final class GiftSetupScreenComponent: Component { containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) ) self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor - self.buttonSeparator.opacity = Float(bottomPanelAlpha) if let view = self.buttonBackground.view { if view.superview == nil { self.addSubview(view) self.layer.addSublayer(self.buttonSeparator) } - view.alpha = bottomPanelAlpha view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize) self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ff09e4d3a4..039a81c650 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -916,7 +916,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } let introController = context.sharedContext.makeStarsIntroScreen(context: context) - introController.navigationPresentation = .modal self.push(introController) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 4becea5f6d..63d7c3003f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -354,8 +354,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } - let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) - if bottomOffset < 100.0 { + let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) + if bottomContentOffset < 100.0 { self.profileGifts.loadMore() } } diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift index 0bee05ace1..5bc9281d03 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift @@ -108,11 +108,9 @@ private final class ArchiveInfoSheetContentComponent: Component { } contentHeight += buttonSize.height - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 14.0 - } + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + contentHeight += bottomInset return CGSize(width: availableSize.width, height: contentHeight) } @@ -226,7 +224,7 @@ private final class ArchiveInfoScreenComponent: Component { }) } )), - backgroundColor: .color(environment.theme.list.plainBackgroundColor), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD index 598da6fc38..61bc2e9d45 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD @@ -31,11 +31,9 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", - "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/BundleIconComponent", - "//submodules/Components/SolidRoundedButtonComponent", "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift index 6789d813fb..fb4e0e9d03 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -8,18 +8,17 @@ import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent -import ScrollComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent -import SolidRoundedButtonComponent +import ButtonComponent import AccountContext -import ScrollComponent +import SheetComponent import BlurredBackgroundComponent import PremiumStarComponent -private final class ScrollContent: CombinedComponent { - typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let openExamples: () -> Void @@ -35,7 +34,7 @@ private final class ScrollContent: CombinedComponent { self.dismiss = dismiss } - static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool { + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } @@ -44,10 +43,10 @@ private final class ScrollContent: CombinedComponent { static var body: Body { let star = Child(PremiumStarComponent.self) - let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let list = Child(List.self) + let actionButton = Child(ButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -82,13 +81,13 @@ private final class ScrollContent: CombinedComponent { UIColor(rgb: 0xfdd219) ], particleColor: UIColor(rgb: 0xf9b004), - backgroundColor: environment.theme.list.plainBackgroundColor + backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition ) context.add(star - .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.navigationHeight + 24.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 79.0)) ) let title = title.update( @@ -193,15 +192,40 @@ private final class ScrollContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) ) contentSize.height += list.size.height - contentSize.height += spacing - 9.0 + contentSize.height += spacing - contentSize.height += 12.0 + 50.0 - if environment.safeInsets.bottom > 0 { - contentSize.height += environment.safeInsets.bottom + 5.0 - } else { - contentSize.height += 12.0 - } - + let buttonHeight: CGFloat = 50.0 + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + + contentSize.height += bottomPanelPadding + + let controller = environment.controller() as? StarsIntroScreen + let actionButton = actionButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: strings.Stars_Info_Done, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { + controller?.dismissAnimated() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: buttonHeight), + transition: context.transition + ) + context.add(actionButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0)) + .cornerRadius(10.0) + ) + contentSize.height += actionButton.size.height + bottomInset + return contentSize } } @@ -238,129 +262,58 @@ private final class ContainerComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) - let scroll = Child(ScrollComponent.self) - let bottomPanel = Child(BlurredBackgroundComponent.self) - let bottomSeparator = Child(Rectangle.self) - let actionButton = Child(SolidRoundedButtonComponent.self) - let scrollExternalState = ScrollComponent.ExternalState() - + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + return { context in let environment = context.environment[EnvironmentType.self] - let theme = environment.theme - let strings = environment.strings - let state = context.state let controller = environment.controller - let background = background.update( - component: Rectangle(color: environment.theme.list.plainBackgroundColor), - environment: {}, - availableSize: context.availableSize, - transition: context.transition - ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - let scroll = scroll.update( - component: ScrollComponent( - content: AnyComponent(ScrollContent( + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( context: context.component.context, openExamples: context.component.openExamples, dismiss: { controller()?.dismiss() } )), - externalState: scrollExternalState, - contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0), - contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in - state?.topContentOffset = topContentOffset - state?.bottomContentOffset = bottomContentOffset - Queue.mainQueue().justDispatch { - state?.updated(transition: .immediate) - } - }, - contentOffsetWillCommit: { targetContentOffset in - } + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut ), - environment: { environment }, + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, availableSize: context.availableSize, transition: context.transition ) - context.add(scroll + context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let bottomPanelAlpha: CGFloat - if scrollExternalState.contentHeight > context.availableSize.height { - if let bottomContentOffset = state.bottomContentOffset { - bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 - } else { - bottomPanelAlpha = 1.0 - } - } else { - bottomPanelAlpha = 0.0 - } - - let bottomPanel = bottomPanel.update( - component: BlurredBackgroundComponent( - color: theme.rootController.tabBar.backgroundColor - ), - availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight), - transition: context.transition - ) - let bottomSeparator = bottomSeparator.update( - component: Rectangle( - color: theme.rootController.tabBar.separatorColor - ), - availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), - transition: context.transition - ) - - context.add(bottomPanel - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) - .opacity(bottomPanelAlpha) - ) - context.add(bottomSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) - .opacity(bottomPanelAlpha) - ) - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let actionButton = actionButton.update( - component: SolidRoundedButtonComponent( - title: strings.Stars_Info_Done, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: theme.list.itemCheckColors.fillColor, - backgroundColors: [], - foregroundColor: theme.list.itemCheckColors.foregroundColor - ), - font: .bold, - fontSize: 17.0, - height: buttonHeight, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - action: { - controller()?.dismiss() - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - context.add(actionButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0)) - ) - return context.availableSize } } @@ -389,7 +342,7 @@ public final class StarsIntroScreen: ViewControllerComponentContainer { theme: forceDark ? .dark : .default ) - self.navigationPresentation = .modal + self.navigationPresentation = .flatModal openExamplesImpl = { [weak self] in guard let self else { @@ -408,6 +361,12 @@ public final class StarsIntroScreen: ViewControllerComponentContainer { required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } } private final class ParagraphComponent: CombinedComponent { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 62a93d5716..ed5fd15396 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -732,7 +732,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, animateOut: animateOut diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index fcf4852d38..7b9c5ce639 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -52,7 +52,6 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) let amountSection = Child(ListSectionComponent.self) @@ -74,15 +73,6 @@ private final class SheetContent: CombinedComponent { let sideInset: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 18.0) - - let background = background.update( - component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), - availableSize: CGSize(width: context.availableSize.width, height: 1000.0), - transition: .immediate - ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) - ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 @@ -466,7 +456,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, From d47bbbeea08007f4f19940faa29238d0bb386dd0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 11:48:12 +0400 Subject: [PATCH 14/19] Various fixes --- .../BrowserUI/Sources/BrowserScreen.swift | 13 + .../BrowserUI/Sources/BrowserWebContent.swift | 307 ++++++++++++-- .../MultilineTextWithEntitiesComponent.swift | 47 ++- .../Sources/InvisibleInkDustNode.swift | 2 +- .../ChatMessageGiftBubbleContentNode.swift | 40 +- .../Sources/GiftSetupScreen.swift | 11 +- .../Sources/RemainingCountComponent.swift | 378 +++++++++++------- .../Sources/GiftViewScreen.swift | 3 +- .../Sources/TextNodeWithEntities.swift | 2 +- 9 files changed, 599 insertions(+), 204 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 6e0d4043fc..a6023a90d4 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -500,6 +500,7 @@ public class BrowserScreen: ViewController, MinimizableController { case closeAddressBar case navigateTo(String, Bool) case expand + case saveToFiles } final class Node: ViewControllerTracingNode { @@ -793,6 +794,10 @@ public class BrowserScreen: ViewController, MinimizableController { if let content = self.content.last { content.resetScrolling() } + case .saveToFiles: + if let content = self.content.last as? BrowserWebContent { + content.requestSaveToFiles() + } } } @@ -1213,6 +1218,14 @@ public class BrowserScreen: ViewController, MinimizableController { performAction.invoke(.addBookmark) action(.default) }))) + + if contentState.contentType == .webPage { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_SaveToFiles, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.saveToFiles) + action(.default) + }))) + } + if !layout.metrics.isTablet && canOpenIn { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in if let self { diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 46311dd2ed..9835fdfe84 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -22,6 +22,7 @@ import UrlHandling import SaveProgressScreen import DeviceModel import LegacyMediaPickerUI +import PassKit private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -213,6 +214,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.presentationData = presentationData var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? + var handleContentMessageImpl: ((WKScriptMessage) -> Void)? + var handleBlobMessageImpl: ((WKScriptMessage) -> Void)? let configuration: WKWebViewConfiguration if let preferredConfiguration { @@ -242,7 +245,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU contentController.add(WeakScriptMessageHandler { message in handleScriptMessageImpl?(message) }, name: "performAction") - + contentController.add(WeakScriptMessageHandler { message in + handleContentMessageImpl?(message) + }, name: "contentInterface") + contentController.add(WeakScriptMessageHandler { message in + handleBlobMessageImpl?(message) + }, name: "blobInterface") configuration.userContentController = contentController configuration.applicationNameForUserAgent = computedUserAgent() } @@ -323,6 +331,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU handleScriptMessageImpl = { [weak self] message in self?.handleScriptMessage(message) } + handleContentMessageImpl = { [weak self] message in + self?.handleContentRequest(message) + } + handleBlobMessageImpl = { [weak self] message in + self?.handleBlobRequest(message) + } } required init?(coder: NSCoder) { @@ -342,13 +356,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } private func handleScriptMessage(_ message: WKScriptMessage) { - guard let body = message.body as? [String: Any] else { + guard let body = message.body as? [String: Any], let eventName = body["eventName"] as? String else { return } - guard let eventName = body["eventName"] as? String else { - return - } - switch eventName { case "cancellingTouch": self.cancelInteractiveTransitionGestures() @@ -357,6 +367,35 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + private func handleContentRequest(_ message: WKScriptMessage) { + guard let string = message.body as? String else { + return + } + guard let data = Data(base64Encoded: string, options: [.ignoreUnknownCharacters]) else { + return + } + guard let url = URL(string: self._state.url) else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let fileName: String + if !url.lastPathComponent.isEmpty { + fileName = url.lastPathComponent + } else { + fileName = "default" + } + + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + let fileUrl = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData if #available(iOS 15.0, *) { @@ -735,13 +774,17 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU @available(iOS 13.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { - self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in - if download { - decisionHandler(.download, preferences) - } else { - decisionHandler(.cancel, preferences) - } - }) + if navigationAction.request.url?.scheme == "blob" { + decisionHandler(.allow, preferences) + } else { + self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in + if download { + decisionHandler(.download, preferences) + } else { + decisionHandler(.cancel, preferences) + } + }) + } } else { if let url = navigationAction.request.url?.absoluteString { if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { @@ -766,14 +809,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if navigationResponse.canShowMIMEType { decisionHandler(.allow) } else if #available(iOS 14.5, *) { -// decisionHandler(.download) - self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in - if download { - decisionHandler(.download) - } else { + if navigationResponse.response.suggestedFilename?.lowercased().hasSuffix(".pkpass") == true { + decisionHandler(.download) + } else { + if let url = navigationResponse.response.url, url.scheme == "blob" { decisionHandler(.cancel) + self.requestBlobSaveToFiles(url: url) + } else { + self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in + if download { + decisionHandler(.download) + } else { + decisionHandler(.cancel) + } + }) } - }) + } } else { decisionHandler(.cancel) } @@ -838,10 +889,23 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let tempFile = TempBox.shared.file(path: path, fileName: fileName) let url = URL(fileURLWithPath: tempFile.path) - let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in - - }) - self.present(controller, nil) + if fileName.hasSuffix(".pkpass") { + if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) { + let passLibrary = PKPassLibrary() + if passLibrary.containsPass(pass) { + //TODO:localize + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: "This pass is already added to Wallet.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})]) + self.present(alertController, nil) + } else if let controller = PKAddPassesViewController(pass: pass) { + self.getNavigationController()?.view.window?.rootViewController?.present(controller, animated: true) + } + } + } else { + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } self.downloadArguments = nil self.downloadProgressObserver = nil @@ -855,28 +919,35 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if let url = webView.url, !url.absoluteString.contains("beatsnvibes") { + guard [NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest].contains(challenge.protectionSpace.authenticationMethod) else { completionHandler(.performDefaultHandling, nil) return } var completed = false let host = webView.url?.host ?? "" - let authController = authController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, title: "Sign in to \(host)", text: "Your login information will be sent securely.", apply: { result in - if !completed { - completed = true - if let (login, password) = result { - let credential = URLCredential( - user: login, - password: password, - persistence: .permanent - ) - completionHandler(.useCredential, credential) - } else { - completionHandler(.cancelAuthenticationChallenge, nil) + + let authController = authController( + sharedContext: self.context.sharedContext, + updatedPresentationData: nil, + title: self.presentationData.strings.WebBrowser_AuthChallenge_Title(host).string, + text: self.presentationData.strings.WebBrowser_AuthChallenge_Text, + apply: { result in + if !completed { + completed = true + if let (login, password) = result { + let credential = URLCredential( + user: login, + password: password, + persistence: .permanent + ) + completionHandler(.useCredential, credential) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } } } - }) + ) authController.dismissed = { byOutsideTap in if byOutsideTap { if !completed { @@ -976,6 +1047,168 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU ) } + func requestSaveToFiles() { + self.webView.evaluateJavaScript("document.contentType") { result, _ in + guard let contentType = result as? String else { + return + } + if #available(iOS 14.0, *), contentType == "text/html" { + self.webView.createWebArchiveData { [weak self] result in + guard let self, case let .success(data) = result else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let tempFile = TempBox.shared.file(path: path, fileName: "\(self._state.title).webarchive") + let url = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + } else { + let s = """ + var xhr = new XMLHttpRequest(); + xhr.open('GET', "\(self._state.url)", true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function(e) { + if (this.status == 200) { + var uInt8Array = new Uint8Array(this.response); + var i = uInt8Array.length; + var binaryString = new Array(i); + while (i--){ + binaryString[i] = String.fromCharCode(uInt8Array[i]); + } + var data = binaryString.join(''); + var base64 = window.btoa(data); + + window.webkit.messageHandlers.contentInterface.postMessage(base64); + } + }; + xhr.send(); + """ + self.webView.evaluateJavaScript(s) + } + } + } + + struct BlobComponents: Codable { + let mimeType: String + let size: Int64 + let dataString: String + } + + func requestBlobSaveToFiles(url: URL) { + guard #available(iOS 14.0, *) else { + return + } + let script = """ + async function createBlobFromUrl(url) { + const response = await fetch(url); + const blob = await response.blob(); + return blob; + } + + function blobToDataURLAsync(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } + + const url = await createBlobFromUrl(blobUrl) + return await blobToDataURLAsync(url) + """ + + self.webView.callAsyncJavaScript(script, + arguments: ["blobUrl": url.absoluteString], + in: nil, + in: WKContentWorld.defaultClient) { result in + switch result { + case .success(let dataUrl): + guard let url = URL(string: dataUrl as! String) else { + print("Failed to get data") + return + } + guard let data = try? Data(contentsOf: url) else { + print("Failed to decode data URL") + return + } + + print(data) + // Do anything with the data. It was a pdf on my case. + //So I used UIDocumentInteractionController to show the pdf + case .failure(let error): + print("Failed with: \(error)") + } + } + +// let urlString = url.absoluteString +// let s = """ +// function blobToDataURL(blob, callback) { +// var reader = new FileReader() +// reader.onload = function(e) {callback(e.target.result.split(",")[1])} +// reader.readAsDataURL(blob) +// } +// async function run() { +// const url = "\(urlString)" +// const blob = await fetch(url).then(r => r.blob()) +// +// blobToDataURL(blob, datauri => { +// const responseObj = { +// mimeType: blob.type, +// size: blob.size, +// dataString: datauri +// } +// window.webkit.messageHandlers.jsListener.postMessage(JSON.stringify(responseObj)) +// }) +// } +// run() +// """ +// self.webView.evaluateJavaScript(s) + } + + private func handleBlobRequest(_ message: WKScriptMessage) { + guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else { + return + } + + let decoder = JSONDecoder() + guard let file = try? decoder.decode(BlobComponents.self, from: jsonData) else { + return + } + guard let data = Data(base64Encoded: file.dataString, options: [.ignoreUnknownCharacters]) else { + return + } + guard let url = URL(string: self._state.url) else { + return + } + let path = NSTemporaryDirectory() + NSUUID().uuidString + let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) + + let fileName: String + if !url.lastPathComponent.isEmpty { + fileName = url.lastPathComponent + } else { + fileName = "default" + } + + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + let fileUrl = URL(fileURLWithPath: tempFile.path) + + let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in + + }) + self.present(controller, nil) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { if [-1003, -1100].contains((error as NSError).code) { if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") { diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index f6e54bbac5..e86e95faad 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textShadowColor: UIColor? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let handleSpoilers: Bool public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -50,6 +51,7 @@ public final class MultilineTextWithEntitiesComponent: Component { textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + handleSpoilers: Bool = false, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -70,6 +72,7 @@ public final class MultilineTextWithEntitiesComponent: Component { self.textStroke = textStroke self.highlightColor = highlightColor self.highlightAction = highlightAction + self.handleSpoilers = handleSpoilers self.tapAction = tapAction self.longTapAction = longTapAction } @@ -99,7 +102,9 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.insets != rhs.insets { return false } - + if lhs.handleSpoilers != rhs.handleSpoilers { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -131,6 +136,7 @@ public final class MultilineTextWithEntitiesComponent: Component { } public final class View: UIView { + var spoilerTextNode: ImmediateTextNodeWithEntities? let textNode: ImmediateTextNodeWithEntities public override init(frame: CGRect) { @@ -197,6 +203,45 @@ public final class MultilineTextWithEntitiesComponent: Component { let size = self.textNode.updateLayout(availableSize) self.textNode.frame = CGRect(origin: .zero, size: size) + if component.handleSpoilers { + let spoilerTextNode: ImmediateTextNodeWithEntities + if let current = self.spoilerTextNode { + spoilerTextNode = current + } else { + spoilerTextNode = ImmediateTextNodeWithEntities() + spoilerTextNode.alpha = 0.0 + self.spoilerTextNode = spoilerTextNode + + self.textNode.dustNode?.textNode = spoilerTextNode + } + + spoilerTextNode.displaySpoilers = true + spoilerTextNode.displaySpoilerEffect = false + spoilerTextNode.attributedText = attributedString + spoilerTextNode.maximumNumberOfLines = component.maximumNumberOfLines + spoilerTextNode.truncationType = component.truncationType + spoilerTextNode.textAlignment = component.horizontalAlignment + spoilerTextNode.verticalAlignment = component.verticalAlignment + spoilerTextNode.lineSpacing = component.lineSpacing + spoilerTextNode.cutout = component.cutout + spoilerTextNode.insets = component.insets + spoilerTextNode.textShadowColor = component.textShadowColor + spoilerTextNode.textStroke = component.textStroke + spoilerTextNode.isUserInteractionEnabled = false + + let size = spoilerTextNode.updateLayout(availableSize) + spoilerTextNode.frame = CGRect(origin: .zero, size: size) + + if spoilerTextNode.view.superview == nil { + self.addSubview(spoilerTextNode.view) + } + } else if let spoilerTextNode = self.spoilerTextNode { + self.spoilerTextNode = nil + spoilerTextNode.view.removeFromSuperview() + + self.textNode.dustNode?.textNode = nil + } + return size } } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 0a88ade9d2..2a48c08549 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -360,7 +360,7 @@ public class InvisibleInkDustNode: ASDisplayNode { private var animColor: CGColor? private let enableAnimations: Bool - private weak var textNode: ASDisplayNode? + public weak var textNode: ASDisplayNode? private let textMaskNode: ASDisplayNode private let textSpotNode: ASImageNode diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index becb0dffaf..fc09e566ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode private let subtitleNode: TextNodeWithEntities + private var spoilerSubtitleNode: TextNodeWithEntities? private let textClippingNode: ASDisplayNode private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode @@ -286,6 +287,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) + let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeMeasureTextLayout = TextNode.asyncLayout(nil) @@ -487,6 +489,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let (_, spoilerSubtitleApply) = makeSpoilerSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets(), displaySpoilers: true)) + var canExpand = false var clippedTextHeight: CGFloat = subtitleLayout.size.height if subtitleLayout.numberOfLines > 4 { @@ -633,7 +637,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = buttonTitleApply() let _ = ribbonTextApply() let _ = moreApply() - + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame @@ -642,8 +646,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) - let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) - strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) + let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size) + strongSelf.subtitleNode.textNode.frame = subtitleFrame if isFirstTime { strongSelf.textClippingNode.frame = clippingTextFrame @@ -657,16 +661,31 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil) if !subtitleLayout.spoilers.isEmpty { + let spoilerSubtitleNode = spoilerSubtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) + if strongSelf.spoilerSubtitleNode == nil { + spoilerSubtitleNode.textNode.alpha = 0.0 + spoilerSubtitleNode.textNode.isUserInteractionEnabled = false + strongSelf.spoilerSubtitleNode = spoilerSubtitleNode + + strongSelf.textClippingNode.addSubnode(spoilerSubtitleNode.textNode) + } + spoilerSubtitleNode.textNode.frame = subtitleFrame + let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) - dustNode.isUserInteractionEnabled = false + dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) strongSelf.dustNode = dustNode - strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode) + strongSelf.textClippingNode.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode) } dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0) dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) @@ -881,8 +900,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.labelNode.frame - if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { + if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - self.labelNode.frame.minX, y: point.y - self.labelNode.frame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { @@ -900,6 +918,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } + if let (_, attributes) = self.subtitleNode.textNode.attributesAtPoint(CGPoint(x: point.x - self.textClippingNode.frame.minX, y: point.y - self.textClippingNode.frame.minY)), gesture == .tap { + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], let dustNode = self.dustNode, !dustNode.isRevealed { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + if self.buttonNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .ignore) } else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 510cf6b4dd..027d10a67c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -607,9 +607,10 @@ final class GiftSetupScreenComponent: Component { if case let .starGift(starGift) = component.subject, let availability = starGift.availability { let remains: Int32 = availability.remains - let position = CGFloat(remains) / CGFloat(availability.total) - let remainsString = "\(remains)" - let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) + let total: Int32 = availability.total + let position = CGFloat(remains) / CGFloat(total) + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator) let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( @@ -624,7 +625,9 @@ final class GiftSetupScreenComponent: Component { badgeText: "\(remainsString)", badgePosition: position, badgeGraphPosition: position, - invertProgress: true + invertProgress: true, + leftString: environment.strings.Gift_Send_Left, + groupingSeparator: environment.dateTimeFormat.groupingSeparator )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index bb1ab4199c..bd1e466aba 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -26,6 +26,8 @@ public class RemainingCountComponent: Component { private let badgePosition: CGFloat private let badgeGraphPosition: CGFloat private let invertProgress: Bool + private let leftString: String + private let groupingSeparator: String public init( inactiveColor: UIColor, @@ -39,7 +41,9 @@ public class RemainingCountComponent: Component { badgeText: String?, badgePosition: CGFloat, badgeGraphPosition: CGFloat, - invertProgress: Bool = false + invertProgress: Bool = false, + leftString: String, + groupingSeparator: String ) { self.inactiveColor = inactiveColor self.activeColors = activeColors @@ -53,6 +57,8 @@ public class RemainingCountComponent: Component { self.badgePosition = badgePosition self.badgeGraphPosition = badgeGraphPosition self.invertProgress = invertProgress + self.leftString = leftString + self.groupingSeparator = groupingSeparator } public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { @@ -92,6 +98,12 @@ public class RemainingCountComponent: Component { if lhs.invertProgress != rhs.invertProgress { return false } + if lhs.leftString != rhs.leftString { + return false + } + if lhs.groupingSeparator != rhs.groupingSeparator { + return false + } return true } @@ -118,7 +130,7 @@ public class RemainingCountComponent: Component { private let badgeShapeLayer = CAShapeLayer() private let badgeForeground: SimpleLayer - private let badgeLabel: BadgeLabelView + private var badgeLabel: BadgeLabelView? private let badgeLeftLabel = ComponentView() private let badgeLabelMaskView = UIImageView() @@ -149,11 +161,7 @@ public class RemainingCountComponent: Component { self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() - - self.badgeLabel = BadgeLabelView() - let _ = self.badgeLabel.update(value: "0", transition: .immediate) - self.badgeLabel.mask = self.badgeLabelMaskView - + super.init(frame: frame) self.addSubview(self.container) @@ -163,7 +171,7 @@ public class RemainingCountComponent: Component { self.addSubview(self.badgeView) self.badgeView.layer.addSublayer(self.badgeForeground) - self.badgeView.addSubview(self.badgeLabel) + //self.badgeView.addSubview(self.badgeLabel) self.badgeLabelMaskView.contentMode = .scaleToFill self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in @@ -255,14 +263,14 @@ public class RemainingCountComponent: Component { self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } - if let badgeText = component.badgeText { + if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel { let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) var frameTransition = transition if from == nil { frameTransition = frameTransition.withAnimation(.none) } - let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition) - frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: transition) + frameTransition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize)) } } @@ -274,7 +282,16 @@ public class RemainingCountComponent: Component { let size = CGSize(width: availableSize.width, height: 90.0) - self.badgeLabel.color = component.activeTitleColor + + if self.badgeLabel == nil { + let badgeLabel = BadgeLabelView(groupingSeparator: component.groupingSeparator) + let _ = badgeLabel.update(value: "0", transition: .immediate) + badgeLabel.mask = self.badgeLabelMaskView + self.badgeLabel = badgeLabel + self.badgeView.addSubview(badgeLabel) + } + + self.badgeLabel?.color = component.activeTitleColor let lineHeight: CGFloat = 30.0 let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight)) @@ -293,56 +310,8 @@ public class RemainingCountComponent: Component { rightTextColor = component.activeTitleColor } - if "".isEmpty { - if component.invertProgress { - let innerLeftTitleSize = self.innerLeftTitleLabel.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.inactiveTitle, - font: Font.semibold(15.0), - textColor: component.activeTitleColor - ) - ) - ) - ), - environment: {}, - containerSize: availableSize - ) - if let view = self.innerLeftTitleLabel.view { - if view.superview == nil { - self.activeContainer.addSubview(view) - } - view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize) - } - - let innerRightTitleSize = self.innerRightTitleLabel.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.activeValue, - font: Font.semibold(15.0), - textColor: component.activeTitleColor - ) - ) - ) - ), - environment: {}, - containerSize: availableSize - ) - if let view = self.innerRightTitleLabel.view { - if view.superview == nil { - self.activeContainer.addSubview(view) - } - view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize) - } - } - - let inactiveTitleSize = self.inactiveTitleLabel.update( + if component.invertProgress { + let innerLeftTitleSize = self.innerLeftTitleLabel.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( @@ -350,7 +319,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.inactiveTitle, font: Font.semibold(15.0), - textColor: leftTextColor + textColor: component.activeTitleColor ) ) ) @@ -358,60 +327,14 @@ public class RemainingCountComponent: Component { environment: {}, containerSize: availableSize ) - if let view = self.inactiveTitleLabel.view { + if let view = self.innerLeftTitleLabel.view { if view.superview == nil { - self.container.addSubview(view) + self.activeContainer.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize) } - let inactiveValueSize = self.inactiveValueLabel.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.inactiveValue, - font: Font.semibold(15.0), - textColor: leftTextColor - ) - ) - ) - ), - environment: {}, - containerSize: availableSize - ) - if let view = self.inactiveValueLabel.view { - if view.superview == nil { - self.container.addSubview(view) - } - view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) - } - - let activeTitleSize = self.activeTitleLabel.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.activeTitle, - font: Font.semibold(15.0), - textColor: rightTextColor - ) - ) - ) - ), - environment: {}, - containerSize: availableSize - ) - if let view = self.activeTitleLabel.view { - if view.superview == nil { - self.container.addSubview(view) - } - view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) - } - - let activeValueSize = self.activeValueLabel.update( + let innerRightTitleSize = self.innerRightTitleLabel.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( @@ -419,7 +342,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.activeValue, font: Font.semibold(15.0), - textColor: rightTextColor + textColor: component.activeTitleColor ) ) ) @@ -427,17 +350,109 @@ public class RemainingCountComponent: Component { environment: {}, containerSize: availableSize ) - if let view = self.activeValueLabel.view { + if let view = self.innerRightTitleLabel.view { if view.superview == nil { - self.container.addSubview(view) - - if component.invertProgress { - self.container.bringSubviewToFront(self.activeContainer) - } + self.activeContainer.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize) } } + + let inactiveTitleSize = self.inactiveTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) + } + + let inactiveValueSize = self.inactiveValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveValue, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) + } + + let activeTitleSize = self.activeTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeTitle, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) + } + + let activeValueSize = self.activeValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + + if component.invertProgress { + self.container.bringSubviewToFront(self.activeContainer) + } + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + } var progressTransition: ComponentTransition = .immediate if !transition.animation.isImmediate { @@ -460,14 +475,39 @@ public class RemainingCountComponent: Component { let countWidth: CGFloat if let badgeText = component.badgeText { - countWidth = CGFloat(badgeText.count) * 10.0 + countWidth = getLabelWidth(badgeText) } else { countWidth = 51.0 } - let badgeWidth: CGFloat = countWidth + 20.0 + let badgeSpacing: CGFloat = 4.0 + + let badgeLeftSize = self.badgeLeftLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.leftString, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.badgeLeftLabel.view { + if view.superview == nil { + self.badgeView.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 10.0 + countWidth + badgeSpacing, y: 4.0 + UIScreenPixel), size: badgeLeftSize) + } + + let badgeWidth: CGFloat = countWidth + 20.0 + badgeSpacing + badgeLeftSize.width let badgeSize = CGSize(width: badgeWidth, height: 30.0) - let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0) + let badgeFullSize = CGSize(width: badgeWidth, height: badgeSize.height + 8.0) let tailSize = CGSize(width: 15.0, height: 6.0) let tailRadius: CGFloat = 3.0 self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) @@ -539,9 +579,9 @@ public class RemainingCountComponent: Component { if transition.animation.isImmediate { if component.badgePosition < 0.1 { self.badgeView.alpha = 1.0 - if let badgeText = component.badgeText { - let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate) - transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel { + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: .immediate) + transition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize)) } } else { self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize) @@ -665,6 +705,7 @@ public class RemainingCountComponent: Component { } +private let spaceWidth: CGFloat = 3.0 private let labelWidth: CGFloat = 10.0 private let labelHeight: CGFloat = 30.0 private let labelSize = CGSize(width: labelWidth, height: labelHeight) @@ -674,7 +715,7 @@ final class BadgeLabelView: UIView { private class StackView: UIView { var labels: [UILabel] = [] - var currentValue: Int32 = 0 + var currentValue: Int32? var color: UIColor = .white { didSet { @@ -684,21 +725,27 @@ final class BadgeLabelView: UIView { } } - init() { + init(groupingSeparator: String) { super.init(frame: CGRect(origin: .zero, size: labelSize)) - var height: CGFloat = -labelHeight - for i in -1 ..< 10 { + var height: CGFloat = -labelHeight * 2.0 + for i in -2 ..< 10 { let label = UILabel() - if i == -1 { + let itemWidth: CGFloat + if i == -2 { + label.text = groupingSeparator + itemWidth = spaceWidth + } else if i == -1 { label.text = "9" + itemWidth = labelWidth } else { label.text = "\(i)" + itemWidth = labelWidth } label.textColor = self.color label.font = font label.textAlignment = .center - label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) + label.frame = CGRect(x: 0, y: height, width: itemWidth, height: labelHeight) self.addSubview(label) self.labels.append(label) @@ -710,36 +757,49 @@ final class BadgeLabelView: UIView { fatalError("init(coder:) has not been implemented") } - func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + func update(value: Int32?, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { let previousValue = self.currentValue self.currentValue = value - self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 + self.labels[2].alpha = isFirst && !isLast ? 0.0 : 1.0 - if previousValue == 9 && value < 9 { + if let value { + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } else { self.bounds = CGRect( origin: CGPoint( x: 0.0, - y: -1.0 * labelSize.height + y: -2.0 * labelSize.height ), size: labelSize ) } - - let bounds = CGRect( - origin: CGPoint( - x: 0.0, - y: CGFloat(value) * labelSize.height - ), - size: labelSize - ) - transition.setBounds(view: self, bounds: bounds) } } + private let groupingSeparator: String private var itemViews: [Int: StackView] = [:] - init() { + init(groupingSeparator: String) { + self.groupingSeparator = groupingSeparator + super.init(frame: .zero) self.clipsToBounds = true @@ -762,8 +822,9 @@ final class BadgeLabelView: UIView { let string = value let stringArray = Array(string.map { String($0) }.reversed()) - let totalWidth = CGFloat(stringArray.count) * labelWidth + let totalWidth: CGFloat = getLabelWidth(value) + var rightX: CGFloat = totalWidth var validIds: [Int] = [] for i in 0 ..< stringArray.count { validIds.append(i) @@ -774,18 +835,21 @@ final class BadgeLabelView: UIView { itemView = current } else { itemTransition = transition.withAnimation(.none) - itemView = StackView() + itemView = StackView(groupingSeparator: self.groupingSeparator) itemView.color = self.color self.itemViews[i] = itemView self.addSubview(itemView) } - let digit = Int32(stringArray[i]) ?? 0 + let digit = Int32(stringArray[i]) itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + let itemWidth: CGFloat = digit != nil ? labelWidth : spaceWidth + rightX -= itemWidth + itemTransition.setFrame( view: itemView, - frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight) + frame: CGRect(x: rightX, y: 0.0, width: labelWidth, height: labelHeight) ) } @@ -805,3 +869,15 @@ final class BadgeLabelView: UIView { return CGSize(width: totalWidth, height: labelHeight) } } + +private func getLabelWidth(_ string: String) -> CGFloat { + var totalWidth: CGFloat = 0.0 + for c in string { + if CharacterSet.decimalDigits.contains(c.unicodeScalars[c.unicodeScalars.startIndex]) { + totalWidth += labelWidth + } else { + totalWidth += spaceWidth + } + } + return totalWidth +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 039a81c650..6872f69960 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -374,7 +374,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, text: .plain(attributedText), - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + handleSpoilers: true ) ) )) diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 1419496161..dc3db47119 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -299,7 +299,7 @@ public class ImmediateTextNodeWithEntities: TextNode { public var arguments: TextNodeWithEntities.Arguments? private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] - private var dustNode: InvisibleInkDustNode? + public private(set) var dustNode: InvisibleInkDustNode? public var visibility: Bool = false { didSet { From 13824dd159f6255ba391b8d5841ae199fb381908 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 12:04:52 +0400 Subject: [PATCH 15/19] Various fixes --- .../Sources/ChatListSearchListPaneNode.swift | 4 ++-- .../Sources/ChatListShimmerNode.swift | 2 +- .../Sources/Node/ChatListNode.swift | 20 +++++++++---------- .../TextSizeSelectionController.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 2 +- ...ChatInlineSearchResultsListComponent.swift | 2 +- .../PeerInfoScreenPersonalChannelItem.swift | 4 ++-- ...aticBusinessMessageListItemComponent.swift | 2 +- .../Sources/QuickReplySetupScreen.swift | 2 +- .../ThemeAccentColorControllerNode.swift | 2 +- .../ChatSearchResultsContollerNode.swift | 2 +- .../CommandChatInputContextPanelNode.swift | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4f211c7658..995dcb2fe9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { @@ -4645,7 +4645,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 7319da6dfb..e7d6cfc338 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fc2bb3c91e..696b5a791f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -101,7 +101,7 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void - let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void + let openPremiumGift: ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void let openPremiumManagement: () -> Void let openActiveSessions: () -> Void let openBirthdaySetup: () -> Void @@ -157,7 +157,7 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, - openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void, + openPremiumGift: @escaping ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void, openPremiumManagement: @escaping () -> Void, openActiveSessions: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void, @@ -741,13 +741,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break case let .starsSubscriptionLowBalance(amount, _): @@ -1081,13 +1081,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break case let .starsSubscriptionLowBalance(amount, _): @@ -1710,11 +1710,11 @@ public final class ChatListNode: ListView { } let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) - }, openPremiumGift: { [weak self] birthdays in + }, openPremiumGift: { [weak self] peers, birthdays in guard let self else { return } - if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first { + if peers.count == 1, let peerId = peers.first?.id { let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) |> filter { !$0.isEmpty } |> deliverOnMainQueue).start(next: { giftOptions in diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 0ea046eeaa..2f6c753e4a 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 61fdf2c69f..1d953a2db4 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 8edf85c688..3a524f7488 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -648,7 +648,7 @@ public final class ChatInlineSearchResultsListComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 27bcb14a8a..1759c73b7f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -182,7 +182,7 @@ public final class LoadingOverlayNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 6aeef906f7..a038c5413b 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -186,7 +186,7 @@ final class GreetingMessageListItemComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index bf1e17537d..1475fb6bbb 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -201,7 +201,7 @@ final class QuickReplySetupScreenComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 6eeecc59bd..2955564155 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index ebf77e9191..21892f43af 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -282,7 +282,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index e5741bad8a..719a632d4e 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -156,7 +156,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, From 427f5ac36fbc3c3f77973129cb7e778faa4a9394 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 13:02:20 +0400 Subject: [PATCH 16/19] Update localization --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 3 ++- .../Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2a1e6d8e10..21a92a0a1d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13041,7 +13041,8 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Limited" = "Limited"; -"Gift.Send.Left" = "left"; +"Gift.Send.Left_1" = "left"; +"Gift.Send.Left_any" = "left"; "Gift.Send.ErrorUnknown" = "An error occurred. Please try again."; "Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift."; diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 027d10a67c..9f8c221c98 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -626,7 +626,7 @@ final class GiftSetupScreenComponent: Component { badgePosition: position, badgeGraphPosition: position, invertProgress: true, - leftString: environment.strings.Gift_Send_Left, + leftString: environment.strings.Gift_Send_Left(remains), groupingSeparator: environment.dateTimeFormat.groupingSeparator )), environment: {}, From 36b23e65c531b3dfa67c08b9b6856767db2a479d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 13:33:44 +0400 Subject: [PATCH 17/19] Update localization --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 4 ++-- .../Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 21a92a0a1d..1a858d3864 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13041,8 +13041,8 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Limited" = "Limited"; -"Gift.Send.Left_1" = "left"; -"Gift.Send.Left_any" = "left"; +"Gift.Send.Remains_1" = "%@ left"; +"Gift.Send.Remains_any" = "%@ left"; "Gift.Send.ErrorUnknown" = "An error occurred. Please try again."; "Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift."; diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 9f8c221c98..e503f27700 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -626,7 +626,7 @@ final class GiftSetupScreenComponent: Component { badgePosition: position, badgeGraphPosition: position, invertProgress: true, - leftString: environment.strings.Gift_Send_Left(remains), + leftString: environment.strings.Gift_Send_Remains(remains).replacingOccurrences(of: remainsString, with: "").trimmingCharacters(in: .whitespacesAndNewlines), groupingSeparator: environment.dateTimeFormat.groupingSeparator )), environment: {}, From 8585ec70cb7679611323c5bd95e60720ab517201 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 15:31:20 +0400 Subject: [PATCH 18/19] Various fixes --- .../Sources/PeerInfoGiftsPaneNode.swift | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 63d7c3003f..e05bcfec47 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -109,6 +109,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public override func didLoad() { super.didLoad() + self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.delegate = self } @@ -136,30 +137,32 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0) + let topInset: CGFloat = 60.0 + var validIds: [AnyHashable] = [] - var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize) + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: starsOptionSize) var index: Int32 = 0 for product in starsProducts { - let itemId = AnyHashable(index) - validIds.append(itemId) - - var itemTransition = transition - let visibleItem: ComponentView - if let current = self.starsItems[itemId] { - visibleItem = current - } else { - visibleItem = ComponentView() - self.starsItems[itemId] = visibleItem - itemTransition = .immediate - } - var isVisible = false if visibleBounds.intersects(itemFrame) { isVisible = true } if isVisible { + let itemId = AnyHashable(index) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.starsItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.starsItems[itemId] = visibleItem + itemTransition = .immediate + } + let ribbonText: String? if let availability = product.gift.availability { ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string @@ -246,8 +249,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.starsItems.removeValue(forKey: id) } - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0 - + var bottomScrollInset: CGFloat = 0.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0 if self.peerId == self.context.account.peerId { let transition = ComponentTransition.immediate @@ -321,7 +324,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) - transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomInset + buttonSize.height + 8.0)) + let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 + transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight)) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) let unlockSize = unlockText.update( @@ -345,9 +349,14 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize)) } contentHeight += unlockSize.height + contentHeight += bottomPanelHeight + + bottomScrollInset = bottomPanelHeight - 40.0 } contentHeight += params.bottomInset + self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 50.0, left: 0.0, bottom: bottomScrollInset, right: 0.0) + let contentSize = CGSize(width: params.size.width, height: contentHeight) if self.scrollNode.view.contentSize != contentSize { self.scrollNode.view.contentSize = contentSize @@ -355,7 +364,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) - if bottomContentOffset < 100.0 { + if bottomContentOffset < 200.0 { self.profileGifts.loadMore() } } From 4aefac47ac6dac371d9077083b4f024c2d549b7a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 6 Oct 2024 15:46:22 +0400 Subject: [PATCH 19/19] Various fixes --- .../Sources/TelegramEngine/Payments/BotPaymentForm.swift | 2 -- .../TelegramCore/Sources/TelegramEngine/Payments/Stars.swift | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index a00e0e0c84..e6592d112d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -703,8 +703,6 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa return .fail(.paymentFailed) } else if error.errorDescription == "INVOICE_ALREADY_PAID" { return .fail(.alreadyPaid) - } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { - return .fail(.starGiftOutOfStock) } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 6ae977572f..3e0ed0eab8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1313,6 +1313,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot return .fail(.alreadyPaid) } else if error.errorDescription == "MEDIA_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { + return .fail(.starGiftOutOfStock) } return .fail(.generic) }