From 37c91f89c5197bc82f4b602dfcad0d988205cb18 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 7 Nov 2024 16:32:33 +0100 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/AttachmentContainer.swift | 2 +- .../Sources/AttachmentController.swift | 18 +- .../BrowserUI/Sources/BrowserWebContent.swift | 7 +- .../Navigation/MinimizedContainer.swift | 5 + .../Sources/MoreButtonNode.swift | 25 +- .../TelegramCore/Sources/Utils/JSON.swift | 32 +- .../Sources/TonFormat.swift | 6 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../Components/Gifts/GiftSetupScreen/BUILD | 1 + .../Sources/GiftSetupScreen.swift | 39 +- .../Sources/MinimizedContainer.swift | 14 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 37 +- .../Sources/PeerInfoScreen.swift | 19 +- .../Sources/PeerInfoGiftsPaneNode.swift | 10 +- .../PremiumPeerShortcutComponent.swift | 9 +- .../Sources/StarsImageComponent.swift | 90 ++- .../Sources/StarsTransactionScreen.swift | 87 ++- .../Sources/StarsTransactionsScreen.swift | 27 +- .../Sources/StarsTransferScreen.swift | 53 +- .../Stories/StoryContainerScreen/BUILD | 1 - ...StoryItemSetContainerViewSendMessage.swift | 64 +- .../Chat/Info/Location.imageset/Contents.json | 12 + .../Chat/Info/Location.imageset/location.pdf | 137 ++++ .../Chat/Info/Status.imageset/Contents.json | 12 + .../Chat/Info/Status.imageset/status (3).pdf | Bin 0 -> 41429 bytes .../MinimizeArrow.imageset/Contents.json | 12 + .../miniappminimize_30.pdf | 92 +++ .../Verified.imageset/Contents.json | 12 + .../Verified.imageset/miniappverify_14.pdf | Bin 0 -> 1604 bytes .../Animations/anim_baremoredots.json | 1 + .../Animations/web_backToCancel.json | 1 + .../Chat/ChatControllerOpenWebApp.swift | 16 +- .../ChatControllerOpenAttachmentMenu.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/PostboxKeys.swift | 2 + submodules/WebUI/BUILD | 7 + .../Sources/FullscreenControlsComponent.swift | 352 ++++++++++ .../WebUI/Sources/WebAppController.swift | 641 ++++++++++++++++-- .../WebAppEmojiStatusAlertController.swift | 365 ++++++++++ .../WebAppLocationAlertController.swift | 293 ++++++++ .../WebAppMessageChatPreviewItem.swift | 454 +++++++++++++ .../Sources/WebAppMessagePreviewScreen.swift | 337 +++++++++ .../WebUI/Sources/WebAppPermissions.swift | 98 +++ .../Sources/WebAppSetEmojiStatusScreen.swift | 390 +++++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 23 +- 46 files changed, 3649 insertions(+), 163 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/anim_baremoredots.json create mode 100644 submodules/TelegramUI/Resources/Animations/web_backToCancel.json create mode 100644 submodules/WebUI/Sources/FullscreenControlsComponent.swift create mode 100644 submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift create mode 100644 submodules/WebUI/Sources/WebAppLocationAlertController.swift create mode 100644 submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift create mode 100644 submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift create mode 100644 submodules/WebUI/Sources/WebAppPermissions.swift create mode 100644 submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c8c19facf4..0b0d604f0e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13218,3 +13218,6 @@ Sorry for the inconvenience."; "Gift.Convert.Period.Unavailable.Text" = "Sorry, you can't convert this gift.\n\nStars can only be claimed within %@ after receiving a gift."; "Gift.Convert.Period.Unavailable.Days_1" = "%@ day"; "Gift.Convert.Period.Unavailable.Days_any" = "%@ days"; + +"Gift.Send.TitleTo" = "Gift to %@"; +"Gift.Send.SendShort" = "Send"; diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 1ddaf947db..259199b5f5 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -524,7 +524,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { } var containerTopInset: CGFloat - if isLandscape { + if isLandscape || controllers.last?.isFullscreen == true { containerTopInset = 0.0 containerLayout = layout diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 4653f67ba9..10730e317f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -124,7 +124,7 @@ public protocol AttachmentContainable: ViewController, MinimizableController { var isInnerPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { get } - + func isContainerPanningUpdated(_ panning: Bool) func resetForReuse() @@ -165,6 +165,10 @@ public extension AttachmentContainable { return nil } + var isFullscreen: Bool { + return false + } + var minimizedTopEdgeOffset: CGFloat? { return nil } @@ -363,6 +367,10 @@ public class AttachmentController: ViewController, MinimizableController { public var minimizedIcon: UIImage? { return self.mainController.minimizedIcon } + + public var isFullscreen: Bool { + return self.mainController.isFullscreen + } private final class Node: ASDisplayNode { private weak var controller: AttachmentController? @@ -1268,6 +1276,10 @@ public class AttachmentController: ViewController, MinimizableController { public var ensureUnfocused = true + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + self.node.minimize() + } + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { if self.ensureUnfocused { self.view.endEditing(true) @@ -1397,7 +1409,9 @@ public class AttachmentController: ViewController, MinimizableController { public func makeContentSnapshotView() -> UIView? { let snapshotView = self.view.snapshotView(afterScreenUpdates: false) if let contentSnapshotView = self.mainController.makeContentSnapshotView() { - contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + if !self.mainController.isFullscreen { + contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + } snapshotView?.addSubview(contentSnapshotView) } return snapshotView diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9fa6fab203..7e87fd08a2 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -638,8 +638,11 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.reloadInputViews() } - self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) - + if fullInsets.bottom.isZero { + self.webView.customBottomInset = safeInsets.bottom + } else { + self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom) + } // self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) // self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index 129a9e7b1e..d6ea34f500 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -30,6 +30,7 @@ public protocol MinimizableController: ViewController { var isMinimizable: Bool { get } var minimizedIcon: UIImage? { get } var minimizedProgress: Float? { get } + var isFullscreen: Bool { get } func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) func makeContentSnapshotView() -> UIView? @@ -41,6 +42,10 @@ public protocol MinimizableController: ViewController { } public extension MinimizableController { + var isFullscreen: Bool { + return false + } + var minimizedTopEdgeOffset: CGFloat? { return nil } diff --git a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift index 8122965095..af3ffd7bc6 100644 --- a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift +++ b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift @@ -13,18 +13,27 @@ public final class MoreButtonNode: ASDisplayNode { case search } + private let encircled: Bool private let duration: Double = 0.21 public var iconState: State = .search - init() { - super.init(size: CGSize(width: 30.0, height: 30.0)) + init(size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool) { + self.encircled = encircled - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0)) + super.init(size: size) + + if self.encircled { + self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0)) + } else { + self.iconState = .more + self.trackTo(item: ManagedAnimationItem(source: .local("anim_baremoredots"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0)) + } } func play() { if case .more = self.iconState { - self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) + let animationName = self.encircled ? "anim_moredots" : "anim_baremoredots" + self.trackTo(item: ManagedAnimationItem(source: .local(animationName), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76)) } } @@ -81,6 +90,7 @@ public final class MoreButtonNode: ASDisplayNode { self.update() } } + private let size: CGSize public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { self.color = color @@ -104,15 +114,16 @@ public final class MoreButtonNode: ASDisplayNode { self.iconNode.customColor = color } - public init(theme: PresentationTheme) { + public init(theme: PresentationTheme, size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool = true) { self.theme = theme + self.size = size self.contextSourceNode = ContextReferenceContentNode() self.containerNode = ContextControllerSourceNode() self.containerNode.animateScale = false self.buttonNode = HighlightableButtonNode() - self.iconNode = MoreIconNode() + self.iconNode = MoreIconNode(size: size, encircled: encircled) self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor super.init() @@ -143,7 +154,7 @@ public final class MoreButtonNode: ASDisplayNode { } override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let animationSize = CGSize(width: 30.0, height: 30.0) + let animationSize = self.size let inset: CGFloat = 0.0 let iconFrame = CGRect(origin: CGPoint(x: inset + 6.0, y: floor((constrainedSize.height - animationSize.height) / 2.0) + 1.0), size: animationSize) diff --git a/submodules/TelegramCore/Sources/Utils/JSON.swift b/submodules/TelegramCore/Sources/Utils/JSON.swift index eea17f436c..3251cd2204 100644 --- a/submodules/TelegramCore/Sources/Utils/JSON.swift +++ b/submodules/TelegramCore/Sources/Utils/JSON.swift @@ -50,6 +50,18 @@ extension JSON { return nil } } + + public init?(dictionary: [String: Any]) { + var values: [String: JSON] = [:] + for (key, value) in dictionary { + if let v = JSON(value) { + values[key] = v + } else { + return nil + } + } + self = .dictionary(values) + } } extension JSON: Collection { @@ -125,7 +137,7 @@ extension JSON { get { switch self { case .null: - return 0 + return NSNull() case let .number(value): return value case let .string(value): @@ -172,6 +184,18 @@ extension JSON { } } +extension JSON { + public var string: String? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: self.value) else { + return nil + } + guard let jsonDataString = String(data: jsonData, encoding: .utf8) else { + return nil + } + return jsonDataString + } +} + extension JSON: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Any)...) { self = .dictionary(elements.reduce([String: JSON]()) { (dictionary, element) in @@ -195,6 +219,12 @@ private protocol JSONValue { var jsonValue: JSON { get } } +extension NSNull: JSONElement, JSONValue { + var jsonValue: JSON { + return .null + } +} + extension Int: JSONElement, JSONValue { var jsonValue: JSON { return .number(Double(self)) diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index f5e49e1fd6..ede7945cad 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -48,7 +48,11 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate } if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { - balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))]) + if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: 2, limitedBy: balanceText.endIndex) { + balanceText = String(balanceText[balanceText.startIndex..? switch component.subject { case .premium: @@ -960,6 +952,20 @@ final class GiftSetupScreenComponent: Component { buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize) } + let controller = environment.controller() + if inputHeight > 10.0 { + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: environment.theme.rootController.navigationBar.accentTextColor)) + controller?.navigationItem.rightBarButtonItem = item + } else { + let rightBarButtonItem = UIBarButtonItem(title: environment.strings.Gift_Send_SendShort, style: .done, target: self, action: #selector(self.proceed)) + rightBarButtonItem.isEnabled = buttonIsEnabled + controller?.navigationItem.setRightBarButton(rightBarButtonItem, animated: controller?.navigationItem.rightBarButtonItem == nil) + } + } else { + controller?.navigationItem.setRightBarButton(nil, animated: true) + } + if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) |> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in @@ -1090,7 +1096,6 @@ final class GiftSetupScreenComponent: Component { } } - let previousBounds = self.scrollView.bounds self.recenterOnTag = nil diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index b316bf2f11..321dd2f7fc 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -1077,13 +1077,23 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) if let _ = itemNode.snapshotView { - if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { + if itemNode.item.controller.isFullscreen { + if layout.size.width < layout.size.height { + let snapshotFrame = itemNode.snapshotContainerView.frame.offsetBy(dx: 0.0, dy: 64.0) + transition.updateFrame(view: itemNode.snapshotContainerView, frame: snapshotFrame) + } + } else if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0) transition.updateFrame(view: snapshotView, frame: snapshotFrame) } } - transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in + var maximizeTopInset = 0.0 + if !itemNode.item.controller.isFullscreen { + maximizeTopInset = topInset + } + + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + maximizeTopInset + self.scrollView.contentOffset.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { self.currentTransition = nil diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 647c1d93a2..f0f10d9518 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -15,6 +15,7 @@ import DeviceAccess import PeerInfoVisualMediaPaneNode import PhotoResources import PeerInfoPaneNode +import WebUI enum PeerInfoUpdatingAvatar { case none @@ -386,6 +387,7 @@ final class PeerInfoScreenData { let revenueStatsContext: RevenueStatsContext? let profileGiftsContext: ProfileGiftsContext? let premiumGiftOptions: [PremiumGiftCodeOption] + let webAppPermissions: WebAppPermissionsState? let _isContact: Bool var forceIsContact: Bool = false @@ -434,7 +436,8 @@ final class PeerInfoScreenData { revenueStatsState: RevenueStats?, revenueStatsContext: RevenueStatsContext?, profileGiftsContext: ProfileGiftsContext?, - premiumGiftOptions: [PremiumGiftCodeOption] + premiumGiftOptions: [PremiumGiftCodeOption], + webAppPermissions: WebAppPermissionsState? ) { self.peer = peer self.chatPeer = chatPeer @@ -472,6 +475,7 @@ final class PeerInfoScreenData { self.revenueStatsContext = revenueStatsContext self.profileGiftsContext = profileGiftsContext self.premiumGiftOptions = premiumGiftOptions + self.webAppPermissions = webAppPermissions } } @@ -967,7 +971,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil ) } } @@ -1015,7 +1020,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1310,7 +1316,16 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return (revenueStatsContext, state.stats) } } - + + let webAppPermissions: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer, case let .user(user) = peer, let _ = user.botInfo { + return webAppPermissionsState(context: context, peerId: peerId) + } else { + return .single(nil) + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), @@ -1329,9 +1344,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen privacySettings, starsRevenueContextAndState, revenueContextAndState, - premiumGiftOptions + premiumGiftOptions, + webAppPermissions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1450,7 +1466,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: revenueContextAndState.1, revenueStatsContext: revenueContextAndState.0, profileGiftsContext: profileGiftsContext, - premiumGiftOptions: premiumGiftOptions + premiumGiftOptions: premiumGiftOptions, + webAppPermissions: webAppPermissions ) } case .channel: @@ -1662,7 +1679,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: revenueContextAndState.1, revenueStatsContext: revenueContextAndState.0, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil ) } case let .group(groupId): @@ -1965,7 +1983,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueStatsState: nil, revenueStatsContext: nil, profileGiftsContext: nil, - premiumGiftOptions: [] + premiumGiftOptions: [], + webAppPermissions: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 6354ffbb83..7040216a1e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1209,6 +1209,7 @@ private enum InfoSection: Int, CaseIterable { case personalChannel case peerInfo case balances + case permissions case peerInfoTrailing case peerMembers } @@ -1540,7 +1541,23 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { print() } - + + if let _ = user.botInfo { + //TODO:localize + items[.permissions]!.append(PeerInfoScreenHeaderItem(id: 30, text: "ALLOW ACCESS TO")) +// items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: false, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in +// +// })) + + if data.webAppPermissions?.location?.isRequested == true { + items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: "Geolocation", value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: user.id) { current in + return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: value)) + }.start() + })) + } + } + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { interaction.openEditing() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index feed0fde86..6cdd4a7c86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -317,14 +317,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return nil }) - let scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight) - + var scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight) + let buttonSideInset = sideInset + 16.0 let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0) + let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 + if params.visibleHeight < 110.0 { + scrollOffset -= bottomPanelHeight + } + 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) - 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)) unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) diff --git a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift index 65694c3c57..d75f042acb 100644 --- a/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift +++ b/submodules/TelegramUI/Components/PremiumPeerShortcutComponent/Sources/PremiumPeerShortcutComponent.swift @@ -12,11 +12,18 @@ public final class PremiumPeerShortcutComponent: Component { let context: AccountContext let theme: PresentationTheme let peer: EnginePeer + let icon: TelegramMediaFile? - public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { + public init( + context: AccountContext, + theme: PresentationTheme, + peer: EnginePeer, + icon: TelegramMediaFile? = nil + ) { self.context = context self.theme = theme self.peer = peer + self.icon = icon } public static func ==(lhs: PremiumPeerShortcutComponent, rhs: PremiumPeerShortcutComponent) -> Bool { diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index fc2a18c3aa..8e4fb473f6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -254,6 +254,7 @@ public final class StarsImageComponent: Component { case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) case gift(Int64) + case color(UIColor) public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { switch lhs { @@ -293,6 +294,12 @@ public final class StarsImageComponent: Component { } else { return false } + case let .color(lhsColor): + if case let .color(rhsColor) = rhs, lhsColor == rhsColor { + return true + } else { + return false + } } } } @@ -307,6 +314,7 @@ public final class StarsImageComponent: Component { public let diameter: CGFloat public let backgroundColor: UIColor public let icon: Icon? + public let value: Int64? public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( @@ -316,6 +324,7 @@ public final class StarsImageComponent: Component { diameter: CGFloat, backgroundColor: UIColor, icon: Icon? = nil, + value: Int64? = nil, action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context @@ -324,6 +333,7 @@ public final class StarsImageComponent: Component { self.diameter = diameter self.backgroundColor = backgroundColor self.icon = icon + self.value = value self.action = action } @@ -346,6 +356,9 @@ public final class StarsImageComponent: Component { if lhs.icon != rhs.icon { return false } + if lhs.value != rhs.value { + return false + } return true } @@ -368,10 +381,14 @@ public final class StarsImageComponent: Component { private var dustNode: MediaDustNode? private var button: UIControl? + private var amountIconView: UIImageView? + private var amountBackgroundView = ComponentView() + private let amountView = ComponentView() + private var animationNode: AnimatedStickerNode? private var lockView: UIImageView? - private var countView = ComponentView() + private let countView = ComponentView() private let fetchDisposable = MetaDisposable() private var hiddenMediaDisposable: Disposable? @@ -471,6 +488,21 @@ public final class StarsImageComponent: Component { switch component.subject { case .none: break + case let .color(color): + let imageNode: TransformImageNode + if let current = self.imageNode { + imageNode = current + } else { + imageNode = TransformImageNode() + imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + containerNode.view.addSubview(imageNode.view) + self.imageNode = imageNode + + imageNode.setSignal(solidColorImage(color)) + } + + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() case let .photo(photo): let imageNode: TransformImageNode if let current = self.imageNode { @@ -873,6 +905,62 @@ public final class StarsImageComponent: Component { smallIconOutlineView.removeFromSuperview() } + if let amount = component.value { + let smallIconView: UIImageView + if let current = self.amountIconView { + smallIconView = current + } else { + smallIconView = UIImageView() + self.amountIconView = smallIconView + + smallIconView.image = UIImage(bundleImageName: "Premium/SendStarsPeerBadgeStarIcon")?.withRenderingMode(.alwaysTemplate) + smallIconView.tintColor = .white + } + + let countSize = self.amountView.update( + transition: .immediate, + component: AnyComponent( + Text(text: "\(amount)", font: Font.with(size: 12.0, design: .round, weight: .bold), color: .white) + ), + environment: {}, + containerSize: imageFrame.size + ) + + let iconSize = CGSize(width: 11.0, height: 11.0) + let iconSpacing: CGFloat = 1.0 + + let totalLabelWidth = iconSize.width + iconSpacing + countSize.width + let iconFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0), y: imageFrame.maxY - countSize.height + 4.0), size: iconSize) + smallIconView.frame = iconFrame + + let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) + iconSize.width + iconSpacing, y: imageFrame.maxY - countSize.height + 2.0), size: countSize) + + let amountBackgroundFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) - 7.0, y: imageFrame.maxY - countSize.height - 3.0), size: CGSize(width: totalLabelWidth + 14.0, height: countFrame.height + 10.0)) + + let _ = self.amountBackgroundView.update( + transition: .immediate, + component: AnyComponent( + RoundedRectangle(colors: [UIColor(rgb: 0xffaa01)], cornerRadius: amountBackgroundFrame.height / 2.0, gradientDirection: .horizontal, stroke: 2.0 - UIScreenPixel, strokeColor: component.backgroundColor, size: amountBackgroundFrame.size) + ), + environment: {}, + containerSize: amountBackgroundFrame.size + ) + if let backgroundView = self.amountBackgroundView.view { + if backgroundView.superview == nil { + containerNode.view.addSubview(backgroundView) + } + backgroundView.frame = amountBackgroundFrame + } + + if let countView = self.amountView.view { + if countView.superview == nil { + containerNode.view.addSubview(countView) + containerNode.view.addSubview(smallIconView) + } + countView.frame = countFrame + } + } + if let _ = component.action { if self.button == nil { let button = UIControl(frame: imageFrame) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index dee42dd278..bead20a23f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -154,6 +154,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let table = Child(TableComponent.self) let additional = Child(BalancedTextComponent.self) let status = Child(BalancedTextComponent.self) + let cancelButton = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self) let transactionStatusBackgound = Child(RoundedRectangle.self) @@ -201,7 +202,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { var boostsText: String? let additionalText = strings.Stars_Transaction_Terms var buttonText: String? = strings.Common_OK - var buttonIsDestructive = false + + var cancelButtonText: String? var statusText: String? var statusIsDestructive = false @@ -223,6 +225,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var isSubscription = false var isSubscriber = false var isSubscriptionFee = false + var isBotSubscription = false var isCancelled = false var isReaction = false var giveawayMessageId: MessageId? @@ -256,7 +259,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { transactionPeer = .peer(peer) isSubscriber = true case let .subscription(subscription): - titleText = strings.Stars_Transaction_Subscription_Title + if case let .user(user) = subscription.peer, user.botInfo != nil { + isBotSubscription = true + } + if let title = subscription.title { + titleText = title + } else { + titleText = strings.Stars_Transaction_Subscription_Title + } + photo = subscription.photo + descriptionText = "" count = subscription.pricing.amount date = subscription.untilDate @@ -320,8 +332,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } else { statusText = strings.Stars_Transaction_Subscription_Active(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string - buttonText = strings.Stars_Transaction_Subscription_Cancel - buttonIsDestructive = true + cancelButtonText = strings.Stars_Transaction_Subscription_Cancel + buttonText = strings.Common_OK } } case let .transaction(transaction, parentPeer): @@ -571,7 +583,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject - let imageIcon: StarsImageComponent.Icon? + var imageIcon: StarsImageComponent.Icon? if isGift { imageSubject = .gift(count) } else if !media.isEmpty { @@ -590,6 +602,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageIcon = nil } + + if isSubscription && "".isEmpty { + imageIcon = nil + } + var starChild: _UpdatedChildComponent if let giftAnimation { starChild = gift.update( @@ -690,7 +707,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else if let toPeer { let title: String if isSubscription { - title = strings.Stars_Transaction_Subscription_Subscription + if isBotSubscription { + //TODO:localize + title = "Bot" + } else { + title = strings.Stars_Transaction_Subscription_Subscription + } } else if isSubscriber { title = strings.Stars_Transaction_Subscription_Subscriber } else { @@ -724,6 +746,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) ) )) + if case let .subscription(subscription) = component.subject, let title = subscription.title { + //TODO:localize + tableItems.append(.init( + id: "subscription", + title: "Subscription", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: tableFont, textColor: tableTextColor))) + ) + )) + } } else if let via { tableItems.append(.init( id: "via", @@ -1069,12 +1101,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) } - if let buttonText { - let button = button.update( + if let cancelButtonText { + let cancelButton = cancelButton.update( component: SolidRoundedButtonComponent( - title: buttonText, - theme: buttonIsDestructive ? SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: destructiveColor) : SolidRoundedButtonComponent.Theme(theme: theme), - font: buttonIsDestructive ? .regular : .bold, + title: cancelButtonText, + theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor), + font: .regular, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, @@ -1094,6 +1126,39 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: context.transition ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: cancelButton.size) + context.add(cancelButton + .position(CGPoint(x: cancelButtonFrame.midX, y: cancelButtonFrame.midY)) + ) + originY += cancelButton.size.height + originY += 8.0 + } + + if let buttonText { + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + if isSubscription && cancelButtonText == nil { + component.updateSubscription() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 22e2aa4e50..1c5efbbcf8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -698,8 +698,33 @@ final class StarsTransactionsScreenComponent: Component { isExpired = true } } + + if let title = subscription.title { + let nameComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + )) + )) + var nameGroupComponent: AnyComponent + if let _ = subscription.photo { + nameGroupComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(RoundedRectangle(color: .lightGray, cornerRadius: 3.0, size: CGSize(width: 19.0, height: 19.0)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: nameComponent) + ], spacing: 6.0) + ) + } else { + nameGroupComponent = nameComponent + } + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: nameGroupComponent) + ) + } + titleComponents.append( - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: dateText, font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index ed5fd15396..0b7065258c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -19,6 +19,7 @@ import AccountContext import PresentationDataUtils import StarsImageComponent import ConfettiEffect +import PremiumPeerShortcutComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -263,6 +264,8 @@ private final class SheetContent: CombinedComponent { let star = Child(StarsImageComponent.self) let closeButton = Child(Button.self) let title = Child(Text.self) + let peerShortcut = Child(PremiumPeerShortcutComponent.self) + let text = Child(BalancedTextComponent.self) let button = Child(ButtonComponent.self) let balanceTitle = Child(MultilineTextComponent.self) @@ -297,14 +300,25 @@ private final class SheetContent: CombinedComponent { if let photo = component.invoice.photo { subject = .photo(photo) } else { - subject = .transactionPeer(.peer(peer)) + if "".isEmpty { + subject = .color(.lightGray) + } else { + subject = .transactionPeer(.peer(peer)) + } } } else { subject = .none } + var isBot = false + if case let .user(user) = state.botPeer, user.botInfo != nil { + isBot = true + } + var isSubscription = false - if case .starsChatSubscription = context.component.source { + if case .starsChatSubscription = component.source { + isSubscription = true + } else if "".isEmpty { isSubscription = true } let star = star.update( @@ -314,7 +328,8 @@ private final class SheetContent: CombinedComponent { theme: theme, diameter: 90.0, backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, - icon: isSubscription ? .star : nil + icon: isSubscription && !isBot ? .star : nil, + value: isBot ? component.invoice.totalAmount : nil ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -350,7 +365,11 @@ private final class SheetContent: CombinedComponent { let titleString: String if isSubscription { - titleString = strings.Stars_Transfer_Subscribe_Channel_Title + if isBot { + titleString = "Subscription Name" + } else { + titleString = strings.Stars_Transfer_Subscribe_Channel_Title + } } else { titleString = strings.Stars_Transfer_Title } @@ -365,6 +384,24 @@ private final class SheetContent: CombinedComponent { ) contentSize.height += title.size.height contentSize.height += 13.0 + + if isBot, let peer = state.botPeer { + contentSize.height -= 3.0 + let peerShortcut = peerShortcut.update( + component: PremiumPeerShortcutComponent( + context: component.context, + theme: theme, + peer: peer + ), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(peerShortcut + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) + ) + contentSize.height += peerShortcut.size.height + contentSize.height += 13.0 + } let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) @@ -378,6 +415,8 @@ private final class SheetContent: CombinedComponent { let infoText: String if case .starsChatSubscription = context.component.source { infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string + } else if "".isEmpty { + infoText = "Do you want to subscribe to **Subscription Name** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?" } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 @@ -499,7 +538,11 @@ private final class SheetContent: CombinedComponent { let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) let buttonAttributedString: NSMutableAttributedString if case .starsChatSubscription = component.source { - buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + //TODO:localize + buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + //buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } else if "".isEmpty { + buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } else { buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 4d244dd6fc..c923c845a9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -30,7 +30,6 @@ swift_library( "//submodules/MediaPickerUI", "//submodules/LegacyMediaPickerUI", "//submodules/LocationUI", - "//submodules/WebUI", "//submodules/TelegramUI/Components/ChatScheduleTimeController", "//submodules/TelegramUI/Components/ChatTimerScreen", "//submodules/TextFormat", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index c28cb72788..932be07c77 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -12,7 +12,6 @@ import MediaPickerUI import LegacyMediaPickerUI import LocationUI import ChatEntityKeyboardInputNode -import WebUI import ChatScheduleTimeController import TextFormat import PhoneNumberFormat @@ -1808,37 +1807,38 @@ final class StoryItemSetContainerSendMessage { //TODO:gift controller break case let .app(bot): - let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) - controller.openUrl = { [weak self] url, _, _, _ in - guard let self else { - return - } - let _ = self - //self?.openUrl(url, concealed: true, forceExternal: true) - } - controller.getNavigationController = { [weak view] in - guard let view, let controller = view.component?.controller() else { - return nil - } - return controller.navigationController as? NavigationController - } - controller.completion = { [weak self] in - guard let self else { - return - } - let _ = self - /*if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - }*/ - } - completion(controller, controller.mediaPickerContext) - self.controllerNavigationDisposable.set(nil) + let _ = bot +// let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) +// let theme = component.theme +// let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) +// let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) +// controller.openUrl = { [weak self] url, _, _, _ in +// guard let self else { +// return +// } +// let _ = self +// //self?.openUrl(url, concealed: true, forceExternal: true) +// } +// controller.getNavigationController = { [weak view] in +// guard let view, let controller = view.component?.controller() else { +// return nil +// } +// return controller.navigationController as? NavigationController +// } +// controller.completion = { [weak self] in +// guard let self else { +// return +// } +// let _ = self +// /*if let strongSelf = self { +// strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { +// $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } +// }) +// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() +// }*/ +// } +// completion(controller, controller.mediaPickerContext) +// self.controllerNavigationDisposable.set(nil) default: break } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json new file mode 100644 index 0000000000..8baec95c43 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "location.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf new file mode 100644 index 0000000000..c9fcd9d6cd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf @@ -0,0 +1,137 @@ +%PDF-1.7 + +1 0 obj + << /Filter /FlateDecode + /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +xmTKŠc1 Üû^Œ°åìcÌ4³H74}è’ã'-’Jé©TÒ³ó2¥1Ý௡לë3$ʳ' »ŒZ2ÀLàrü¹¾Æsˆ˜ÉÔˆZ™I8•.(ÂoDäJ}¤Ö”tž­Fî”Ef)ñ‚ÑZjê#²Pm!j©Î‘µÞ&,Ì©÷È“¸Hª ¥JžµŽX¬™Ëìv*gb|9‡¶æÞÙGpûκÿƒ³N?Çfü@žµ׹¡þ®ãg>ÃY}Ï›1³ocës?{ÍÁø½›`¿´y °_îÚƒù·^nß(wo^Ë6ÉG0ïÆ™uc¶óÆ}ÜÆÍîùNíÇë- £›èk›W+·ã|úlð?|„¯ð/|ã¨#‡ðâ7wRàLÍñ‰Ð;iÙW wÒj¶ãJN &¹èBÀ(—MQ(ã‚÷vPY7¢1¶S¯¬*s#Þ‚*³KœÂA¤Ò Kn¤—sêèk¤ùk8˜¬b;ÅHíǹ°áÍ–8Ÿ ­äÖGçrz"¬"‘þ³UÌåÜáä`ëS¦AfÑ¥­¤ú\ ”þ+-i6#ëà9Ssêhë¤ù35›Â𣸸³Ù/ɬ#= +endstream +endobj + +2 0 obj + 470 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 30.000000 30.000000 ] + >> +stream +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 916 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000754 00000 n +0000000776 00000 n +0000001940 00000 n +0000001962 00000 n +0000002260 00000 n +0000002362 00000 n +0000002383 00000 n +0000002556 00000 n +0000002630 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2690 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json new file mode 100644 index 0000000000..7f32403ef0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "status (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf new file mode 100644 index 0000000000000000000000000000000000000000..b6dd968ea1254bbb4091715b7dd0ca625226fc11 GIT binary patch literal 41429 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 G_x}K!8XI*0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json new file mode 100644 index 0000000000..595c2fda0f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniappminimize_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf new file mode 100644 index 0000000000..33da1d357a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.500000 8.804749 cm +0.000000 0.000000 0.000000 scn +0.707107 9.402358 m +0.316583 9.792883 -0.316583 9.792883 -0.707107 9.402358 c +-1.097631 9.011834 -1.097631 8.378669 -0.707107 7.988145 c +0.707107 9.402358 l +h +6.500000 2.195251 m +5.792893 1.488145 l +6.183417 1.097620 6.816583 1.097620 7.207107 1.488145 c +6.500000 2.195251 l +h +13.707107 7.988145 m +14.097631 8.378669 14.097631 9.011834 13.707107 9.402358 c +13.316583 9.792883 12.683417 9.792883 12.292893 9.402358 c +13.707107 7.988145 l +h +-0.707107 7.988145 m +5.792893 1.488145 l +7.207107 2.902358 l +0.707107 9.402358 l +-0.707107 7.988145 l +h +7.207107 1.488145 m +13.707107 7.988145 l +12.292893 9.402358 l +5.792893 2.902358 l +7.207107 1.488145 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 772 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000862 00000 n +0000000884 00000 n +0000001057 00000 n +0000001131 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1190 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json new file mode 100644 index 0000000000..4245696764 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "miniappverify_14.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a8edc9d6d7cea1fc09303f8ec89190cf7f9a4704 GIT binary patch literal 1604 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!aK>2r6@cv`;KajoYLx!5bR zGnc#M?l(ccpe+Y78K z+U5VxSAF{U{=D^v&%fTXZ)!*zk9OgURaW1w*Wcdt?eW9UuZ8Dt<w9hE|&siyd@3-vAl~BLHUBfg+Shb; zM0I4m-r4N&ypmmVQtAP@LaDv1&s?(%^LW>$CF@^of487u+=|t%{$dR;k(+(h|B<{8#O5wjtaLr z#9yWd>|*ZxqISjIx>rw$lWXgoSiTF{&Ostt_c(Sa`o?I!J+aHlH1xe(?HZAeC1DeU zGQ^s^W_+046fbSGaL2?)7iTSfXm{Zcr`50BQ`I>&uI^I2AS|`@P?$q^m4Dj9HU|Ap zi>o>@ds}59#kr3hVHWygy+QKMh2zXx0SCgkUM!h+QShwXI;J^7 zhDmV}t{nSYe6*Z*w1ouc&$;f#_b4#ymeSW1&O2Nm9kv!Pit3RPGurz?Id1+9>1_YY zn{qUIW^(3KSua0ka^Qp%!>^2-=7`jqPRoUz2k+nT)hOu`j#zWRc|q|rxf3$J6Rh+6 zK2+8A{#?aSm?Z6TCosK2JlAXLuNNQBig8s|ev^I^0>G17AYV36xDHvSy*!AL6eQzc`Ai+-0P)3KVN!8guS%H?t8b8(f@%+%F^zk9mx z3!iSD?|UX8<&#uMtjFgwY&#ws@a#T4b%$T4?Dr3W#ilzf>%5mFs2H3O>YW;JQ@rG_ zLyIi8xwhVpnKioen*Z*vzh#$OpA*j*G<91oD91yyEG+MXGBc!1urxM9_Rd0pk6}^_ZLHoF<4-jp_l~nU@_c6ki#GzcTOxx%*jtj)ml-Mn#N_IV9o{c zK8RE>Gc`3fRR9V@fuVr{n5B>h7cvIA69nKwh6bjXLgqk+p{g=6u|yX#HUh>aR8>h) zVrEWi5f>=-JY9gnp;4Tlo2sdxk(r_i2^9UH{QMFHkcYtmtRI|Nl?rq{xJ*whDgk@P O(AdI=OI6j?-wgmK_g}pL literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json b/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json new file mode 100644 index 0000000000..e9c79a9f1a --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/anim_baremoredots.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":300,"w":300,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"more","sr":1,"st":-38,"op":47,"ip":-35,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[150,150],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150,150],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"3","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[50,15],[65,0],[50,-15],[35,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":25},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":35},{"s":[100,100],"t":45}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,150],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,120],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,162],"t":30},{"s":[200,150],"t":40}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"2","ix":3,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 2","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[0.5,15],[15.5,0],[0.5,-15],[-14.5,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":30},{"s":[100,100],"t":40}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,150],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,120],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,162],"t":25},{"s":[150,150],"t":35}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"1","ix":4,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 3","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[-49,15],[-34,0],[-49,-15],[-64,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[-50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":25},{"s":[100,100],"t":35}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,150],"t":0},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,120],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,162],"t":20},{"s":[100,150],"t":30}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/web_backToCancel.json b/submodules/TelegramUI/Resources/Animations/web_backToCancel.json new file mode 100644 index 0000000000..1ae3321c25 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/web_backToCancel.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":99,"w":99,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"Artboard Copy 3 Outlines","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[49.5,49.5,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[49.5,49.5],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":45}],"ix":2}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[100],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[100],"t":30},{"s":[50],"t":45}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[0],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[0],"t":30},{"s":[50],"t":45}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[41.25,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":45}],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[57.75,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 1fec78f193..97da9a68e3 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -103,12 +103,18 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } var fullSize = false + var isFullscreen = false if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - fullSize = !url.contains("?mode=compact") + if url.contains("&mode=fullscreen") { + isFullscreen = true + fullSize = true + } else { + fullSize = !url.contains("?mode=compact") + } } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize) + let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -196,7 +202,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -242,7 +248,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u return } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -472,7 +478,7 @@ public extension ChatControllerImpl { return } let context = strongSelf.context - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen)) var presentImpl: ((ViewController, Any?) -> Void)? let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index a248e12346..ab8901e8b6 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -613,7 +613,7 @@ extension ChatControllerImpl { payload = botPayload fromAttachMenu = false } - let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) + let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false, isFullscreen: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) controller.openUrl = { [weak self] url, concealed, forceUpdate, commit in diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index acb60a6399..32ed35f82d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -308,7 +308,7 @@ func openResolvedUrlImpl( id: subscriptionFormId, canSaveCredentials: false, passwordMissing: false, - invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil), + invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil, subscriptionPeriod: subscriptionPricing.period), paymentBotId: channel.id, providerId: nil, url: nil, diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index ebefd6a4f9..c894a82f00 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -79,6 +79,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { case storySource = 11 case mediaEditorState = 12 case shareWithPeersState = 13 + case webAppPermissionsState = 14 } public struct ApplicationSpecificItemCacheCollectionId { @@ -94,6 +95,7 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let storySource = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.storySource.rawValue) public static let mediaEditorState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaEditorState.rawValue) public static let shareWithPeersState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.shareWithPeersState.rawValue) + public static let webAppPermissionsState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.webAppPermissionsState.rawValue) } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 19b80d186e..49d110863f 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -41,6 +41,13 @@ swift_library( "//submodules/UndoUI", "//submodules/OverlayStatusController", "//submodules/TelegramUIPreferences", + "//submodules/Components/LottieAnimationComponent", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", + "//submodules/DeviceLocationManager", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/FullscreenControlsComponent.swift b/submodules/WebUI/Sources/FullscreenControlsComponent.swift new file mode 100644 index 0000000000..a2764744b3 --- /dev/null +++ b/submodules/WebUI/Sources/FullscreenControlsComponent.swift @@ -0,0 +1,352 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import BundleIconComponent +import MultilineTextComponent +import MoreButtonNode +import AccountContext +import TelegramPresentationData +import LottieAnimationComponent + +final class FullscreenControlsComponent: Component { + let context: AccountContext + let title: String + let isVerified: Bool + let insets: UIEdgeInsets + var hasBack: Bool + let backPressed: () -> Void + let minimizePressed: () -> Void + let morePressed: (ASDisplayNode, ContextGesture?) -> Void + + init( + context: AccountContext, + title: String, + isVerified: Bool, + insets: UIEdgeInsets, + hasBack: Bool, + backPressed: @escaping () -> Void, + minimizePressed: @escaping () -> Void, + morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void + ) { + self.context = context + self.title = title + self.isVerified = isVerified + self.insets = insets + self.hasBack = hasBack + self.backPressed = backPressed + self.minimizePressed = minimizePressed + self.morePressed = morePressed + } + + static func ==(lhs: FullscreenControlsComponent, rhs: FullscreenControlsComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isVerified != rhs.isVerified { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.hasBack != rhs.hasBack { + return false + } + return true + } + + final class View: UIView { + private let leftBackgroundView: BlurredBackgroundView + private let rightBackgroundView: BlurredBackgroundView + + private let closeIcon = ComponentView() + private let leftButton = HighlightTrackingButton() + + private let titleClippingView = UIView() + private let title = ComponentView() + private let credibility = ComponentView() + private let buttonTitle = ComponentView() + private let minimizeButton = ComponentView() + private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false) + + private var displayTitle = true + private var timer: Timer? + + private var component: FullscreenControlsComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.leftBackgroundView = BlurredBackgroundView(color: nil) + self.rightBackgroundView = BlurredBackgroundView(color: nil) + + super.init(frame: frame) + + self.titleClippingView.clipsToBounds = true + self.titleClippingView.isUserInteractionEnabled = false + + self.leftBackgroundView.clipsToBounds = true + self.addSubview(self.leftBackgroundView) + self.addSubview(self.leftButton) + + self.addSubview(self.titleClippingView) + + self.rightBackgroundView.clipsToBounds = true + self.addSubview(self.rightBackgroundView) + + self.addSubview(self.moreNode.view) + + self.moreNode.updateColor(.white, transition: .immediate) + + self.leftButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + if let view = self.closeIcon.view { + view.layer.removeAnimation(forKey: "opacity") + view.alpha = 0.6 + } + if let view = self.buttonTitle.view { + view.layer.removeAnimation(forKey: "opacity") + view.alpha = 0.6 + } + } else { + if let view = self.closeIcon.view { + view.alpha = 1.0 + view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + if let view = self.buttonTitle.view { + view.alpha = 1.0 + view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + self.leftButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + @objc private func closePressed() { + guard let component = self.component else { + return + } + component.backPressed() + } + + @objc private func timerEvent() { + self.timer?.invalidate() + self.timer = nil + + self.displayTitle = false + self.state?.updated(transition: .spring(duration: 0.3)) + } + + func update(component: FullscreenControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let isFirstTime = self.component == nil + let previousComponent = self.component + self.component = component + self.state = state + + let sideInset: CGFloat = 16.0 + let leftBackgroundSize = CGSize(width: 30.0, height: 30.0) + let rightBackgroundSize = CGSize(width: 72.0, height: 30.0) + + self.leftBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition) + self.rightBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition) + + let rightBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - sideInset - rightBackgroundSize.width, y: 0.0), size: rightBackgroundSize) + self.rightBackgroundView.update(size: rightBackgroundSize, cornerRadius: rightBackgroundFrame.height / 2.0, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.rightBackgroundView, frame: rightBackgroundFrame) + + var isAnimatingTextTransition = false + + var additionalLeftWidth: CGFloat = 0.0 + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? 3.0 : -titleSize.width - 15.0, y: floorToScreenPixels((leftBackgroundSize.height - titleSize.height) / 2.0)), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.titleClippingView.addSubview(view) + } + + if !view.alpha.isZero && !self.displayTitle { + isAnimatingTextTransition = true + } + + transition.setFrame(view: view, frame: titleFrame) + transition.setAlpha(view: view, alpha: self.displayTitle ? 1.0 : 0.0) + } + + let buttonTitleUpdated = (previousComponent?.hasBack ?? false) != component.hasBack + let animationMultiplier = !component.hasBack ? -1.0 : 1.0 + if buttonTitleUpdated { + isAnimatingTextTransition = true + + if let view = self.buttonTitle.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = view.frame + self.titleClippingView.addSubview(snapshotView) + snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: -(snapshotView.frame.width * 1.5) * animationMultiplier, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + } + + let buttonTitleSize = self.buttonTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.hasBack ? "Back" : "Close", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))), + environment: {}, + containerSize: availableSize + ) + + if self.displayTitle { + additionalLeftWidth += titleSize.width + 10.0 + } else { + additionalLeftWidth += buttonTitleSize.width + 10.0 + } + + let buttonTitleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? leftBackgroundSize.width + additionalLeftWidth + 3.0 : 3.0, y: floorToScreenPixels((leftBackgroundSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize) + if let view = self.buttonTitle.view { + if view.superview == nil { + self.titleClippingView.addSubview(view) + } + transition.setFrame(view: view, frame: buttonTitleFrame) + + if buttonTitleUpdated { + view.layer.animatePosition(from: CGPoint(x: (view.frame.width * 1.5) * animationMultiplier, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + if component.isVerified { + let credibilitySize = self.credibility.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Instant View/Verified", tintColor: .white)), + environment: {}, + containerSize: availableSize + ) + if let view = self.credibility.view { + if view.superview == nil { + view.alpha = 0.6 + self.titleClippingView.addSubview(view) + } + let credibilityFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels((leftBackgroundSize.height - credibilitySize.height) / 2.0)), size: credibilitySize) + transition.setFrame(view: view, frame: credibilityFrame) + } + if self.displayTitle { + additionalLeftWidth += credibilitySize.width + 2.0 + } + } + + var leftBackgroundTransition = transition + if buttonTitleUpdated { + leftBackgroundTransition = .spring(duration: 0.3) + } + + let leftBackgroundFrame = CGRect(origin: CGPoint(x: sideInset + component.insets.left, y: 0.0), size: CGSize(width: leftBackgroundSize.width + additionalLeftWidth, height: leftBackgroundSize.height)) + self.leftBackgroundView.update(size: leftBackgroundFrame.size, cornerRadius: leftBackgroundSize.height / 2.0, transition: leftBackgroundTransition.containedViewLayoutTransition) + leftBackgroundTransition.setFrame(view: self.leftBackgroundView, frame: leftBackgroundFrame) + self.leftButton.frame = leftBackgroundFrame + + if isAnimatingTextTransition, self.titleClippingView.mask == nil { + if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 0.9, 1.0], direction: .horizontal) { + let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 4, topCapHeight: 0)) + self.titleClippingView.mask = maskView + maskView.frame = CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)) + } + } + + transition.setFrame(view: self.titleClippingView, frame: CGRect(origin: CGPoint(x: sideInset + component.insets.left + leftBackgroundSize.height - 3.0, y: 0.0), size: CGSize(width: leftBackgroundFrame.width - leftBackgroundSize.height, height: leftBackgroundSize.height))) + if let maskView = self.titleClippingView.mask { + leftBackgroundTransition.setFrame(view: maskView, frame: CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)), completion: { _ in + self.titleClippingView.mask = nil + }) + } + + let backButtonSize = self.closeIcon.update( + transition: .immediate, + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "web_backToCancel", + mode: .animating(loop: false), + range: component.hasBack ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ) + ), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + if let view = self.closeIcon.view { + if view.superview == nil { + view.isUserInteractionEnabled = false + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: leftBackgroundFrame.minX, y: 0.0), size: backButtonSize) + transition.setFrame(view: view, frame: buttonFrame) + } + + let minimizeButtonSize = self.minimizeButton.update( + transition: .immediate, + component: AnyComponent(Button( + content: AnyComponent( + BundleIconComponent(name: "Instant View/MinimizeArrow", tintColor: .white) + ), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.minimizePressed() + } + ).minSize(CGSize(width: 30.0, height: 30.0))), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + if let view = self.minimizeButton.view { + if view.superview == nil { + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: rightBackgroundFrame.minX + 2.0, y: 0.0), size: minimizeButtonSize) + transition.setFrame(view: view, frame: buttonFrame) + } + + transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: rightBackgroundFrame.maxX - 42.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0))) + self.moreNode.action = { [weak self] node, gesture in + guard let self, let component = self.component else { + return + } + component.morePressed(node, gesture) + } + + if isFirstTime { + let timer = Timer(timeInterval: 2.5, target: self, selector: #selector(self.timerEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: .common) + } + + return CGSize(width: availableSize.width, height: leftBackgroundSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index b1ac4f8d1f..48c4c656e8 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -6,6 +6,7 @@ import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit +import ComponentFlow import TelegramPresentationData import AccountContext import AttachmentUI @@ -30,6 +31,8 @@ import UndoUI import AvatarNode import OverlayStatusController import TelegramUIPreferences +import CoreMotion +import DeviceLocationManager private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -64,6 +67,7 @@ public struct WebAppParameters { let keepAliveSignal: Signal? let forceHasSettings: Bool let fullSize: Bool + let isFullscreen: Bool public init( source: Source, @@ -77,7 +81,8 @@ public struct WebAppParameters { buttonText: String?, keepAliveSignal: Signal?, forceHasSettings: Bool, - fullSize: Bool + fullSize: Bool, + isFullscreen: Bool = false ) { self.source = source self.peerId = peerId @@ -91,6 +96,11 @@ public struct WebAppParameters { self.keepAliveSignal = keepAliveSignal self.forceHasSettings = forceHasSettings self.fullSize = fullSize +// #if DEBUG +// self.isFullscreen = true +// #else + self.isFullscreen = isFullscreen +// #endif } } @@ -136,6 +146,7 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate var webView: WebAppWebView? private var placeholderIcon: (UIImage, Bool)? private var placeholderNode: ShimmerEffectNode? + private var fullscreenControls: ComponentView? fileprivate let loadingProgressPromise = Promise(nil) @@ -158,6 +169,8 @@ public final class WebAppController: ViewController, AttachmentContainable { private var queryId: Int64? fileprivate let canMinimize = true + private var hasBackButton = false + private var placeholderDisposable = MetaDisposable() private var keepAliveDisposable: Disposable? private var paymentDisposable: Disposable? @@ -180,7 +193,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.backgroundNode = ASDisplayNode() self.headerBackgroundNode = ASDisplayNode() self.topOverscrollNode = ASDisplayNode() - + super.init() if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 { @@ -321,6 +334,13 @@ public final class WebAppController: ViewController, AttachmentContainable { self.paymentDisposable?.dispose() self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + + if self.motionManager.isAccelerometerActive { + self.motionManager.stopAccelerometerUpdates() + } + if self.motionManager.isGyroActive { + self.motionManager.stopGyroUpdates() + } } override func didLoad() { @@ -593,7 +613,15 @@ public final class WebAppController: ViewController, AttachmentContainable { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousLayout = self.validLayout?.0 self.validLayout = (layout, navigationBarHeight) - + + guard let controller = self.controller else { + return + } + + self.controller?.navigationBar?.alpha = controller.isFullscreen ? 0.0 : 1.0 + transition.updateAlpha(node: self.topOverscrollNode, alpha: controller.isFullscreen ? 0.0 : 1.0) + transition.updateAlpha(node: self.headerBackgroundNode, alpha: controller.isFullscreen ? 0.0 : 1.0) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size)) transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0))) @@ -606,8 +634,10 @@ public final class WebAppController: ViewController, AttachmentContainable { scrollInset.bottom = 0.0 } - let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) - if !webView.frame.width.isZero && webView.frame != frame { + let topInset: CGFloat = controller.isFullscreen ? 0.0 : navigationBarHeight + + let webViewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - topInset - frameBottomInset))) + if !webView.frame.width.isZero && webView.frame != webViewFrame { self.updateWebViewWhenStable = true } @@ -615,7 +645,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { bottomInset = max(bottomInset, inputHeight) } - let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - bottomInset))) + let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset))) if webView.scrollView.contentInset != scrollInset { webView.scrollView.contentInset = scrollInset @@ -628,16 +658,29 @@ public final class WebAppController: ViewController, AttachmentContainable { }, transition: transition) Queue.mainQueue().after(0.4, { if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { - transition.updateFrame(view: webView, frame: frame) + transition.updateFrame(view: webView, frame: webViewFrame) Queue.mainQueue().after(0.1) { self.targetContentOffset = nil } } }) } else { - transition.updateFrame(view: webView, frame: frame) + transition.updateFrame(view: webView, frame: webViewFrame) } + var customInsets: UIEdgeInsets = .zero + if controller.isFullscreen { + customInsets.top = layout.statusBarHeight ?? 0.0 + } + if layout.intrinsicInsets.bottom > 44.0 { + customInsets.bottom = 0.0 + } else { + customInsets.bottom = layout.intrinsicInsets.bottom + } + customInsets.left = layout.safeInsets.left + customInsets.right = layout.safeInsets.left + webView.customInsets = customInsets + if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) if self.updateWebViewWhenStable && !controller.isContainerPanning() { @@ -645,13 +688,6 @@ public final class WebAppController: ViewController, AttachmentContainable { webView.setNeedsLayout() } } - - if layout.intrinsicInsets.bottom > 44.0 { - webView.customBottomInset = 0.0 - } else { - webView.customBottomInset = layout.intrinsicInsets.bottom - } - webView.customSideInset = layout.safeInsets.left } if let placeholderNode = self.placeholderNode { @@ -674,6 +710,66 @@ public final class WebAppController: ViewController, AttachmentContainable { placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size) } + if controller.isFullscreen { + var added = false + let fullscreenControls: ComponentView + if let current = self.fullscreenControls { + fullscreenControls = current + } else { + fullscreenControls = ComponentView() + self.fullscreenControls = fullscreenControls + added = true + } + + let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition) + let controlsSize = fullscreenControls.update( + transition: componentTransition, + component: AnyComponent( + FullscreenControlsComponent( + context: self.context, + title: controller.botName, + isVerified: controller.botVerified, + insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), + hasBack: self.hasBackButton, + backPressed: { [weak self] in + guard let self else { + return + } + self.controller?.cancelPressed() + }, + minimizePressed: { [weak self] in + guard let self else { + return + } + self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) + }, + morePressed: { [weak self] node, gesture in + guard let self else { + return + } + self.controller?.morePressed(node: node, gesture: gesture) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + if let view = fullscreenControls.view { + if view.superview == nil { + self.view.addSubview(view) + } + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + 8.0), size: controlsSize)) + if added { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + } else if let fullscreenControls = self.fullscreenControls { + self.fullscreenControls = nil + fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + fullscreenControls.view?.removeFromSuperview() + }) + } + if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 { Queue.mainQueue().justDispatch { self.controller?.requestAttachmentMenuExpansion() @@ -691,6 +787,12 @@ public final class WebAppController: ViewController, AttachmentContainable { private weak var currentQrCodeScannerScreen: QrCodeScanScreen? + func requestLayout(transition: ContainedViewLayoutTransition) { + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + private var delayedScriptMessages: [WKScriptMessage] = [] private func handleScriptMessage(_ message: WKScriptMessage) { guard let controller = self.controller else { @@ -786,9 +888,9 @@ public final class WebAppController: ViewController, AttachmentContainable { } } case "web_app_request_viewport": - if let (layout, navigationBarHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } + self.requestLayout(transition: .immediate) + case "web_app_request_safe_area": + self.requestLayout(transition: .immediate) case "web_app_request_theme": self.sendThemeChangedEvent() case "web_app_expand": @@ -939,7 +1041,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } case "web_app_setup_back_button": if let json = json, let isVisible = json["is_visible"] as? Bool { + self.hasBackButton = isVisible self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true) + if controller.isFullscreen { + self.requestLayout(transition: .immediate) + } } case "web_app_trigger_haptic_feedback": if let json = json, let type = json["type"] as? String { @@ -1234,6 +1340,43 @@ public final class WebAppController: ViewController, AttachmentContainable { } }) } + case "web_app_request_fullscreen": + self.setIsFullscreen(true) + case "web_app_exit_fullscreen": + self.setIsFullscreen(false) + case "web_app_start_accelerometer": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsAccelerometerActive(true, refreshRate: refreshRate) + } + case "web_app_stop_accelerometer": + self.setIsAccelerometerActive(false) + case "web_app_start_device_orientation": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsDeviceOrientationActive(true, refreshRate: refreshRate) + } + case "web_app_stop_device_orientation": + self.setIsDeviceOrientationActive(false) + case "web_app_start_gyroscope": + if let json = json, let refreshRate = json["refresh_rate"] as? Double { + self.setIsGyroscopeActive(true, refreshRate: refreshRate) + } + case "web_app_stop_gyroscope": + self.setIsGyroscopeActive(false) + case "web_app_set_emoji_status": + if let json = json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { + let expirationDate = json["expiration_date"] as? Double + self.setEmojiStatus(emojiId, expirationDate: expirationDate.flatMap { Int32($0) }) + } + case "web_app_add_to_home_screen": + self.addToHomeScreen() + case "web_app_check_home_screen": + self.webView?.sendEvent(name: "home_screen_checked", data: "{status: \"unknown\"}") + case "web_app_request_location": + self.requestLocation() + case "web_app_check_location": + self.checkLocation() + case "web_app_open_location_settings": + break default: break } @@ -1469,11 +1612,16 @@ public final class WebAppController: ViewController, AttachmentContainable { } fileprivate func shareAccountContact() { - guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else { + if "".isEmpty, let controller = self.controller { + let previewController = WebAppMessagePreviewScreen(context: controller.context, completion: { _ in }) + previewController.navigationPresentation = .flatModal + controller.parentController()?.push(previewController) return } - + guard let context = self.controller?.context, let botId = self.controller?.botId, let botName = self.controller?.botName else { + return + } let sendEvent: (Bool) -> Void = { success in var paramsString: String if success { @@ -1484,7 +1632,6 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "phone_requested", data: paramsString) } - let context = self.context let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId) @@ -1858,6 +2005,392 @@ public final class WebAppController: ViewController, AttachmentContainable { navigationController.pushViewController(settingsController) } } + + fileprivate func setIsFullscreen(_ isFullscreen: Bool) { + guard let controller = self.controller else { + return + } + guard controller.isFullscreen != isFullscreen else { + self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}") + return + } + + let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}" + self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString) + + controller.isFullscreen = isFullscreen + (controller.parentController() as? AttachmentController)?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + + private let motionManager = CMMotionManager() + private var isAccelerometerActive = false + fileprivate func setIsAccelerometerActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isAccelerometerAvailable else { + self.webView?.sendEvent(name: "accelerometer_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isAccelerometerActive != isActive else { + return + } + self.isAccelerometerActive = isActive + if isActive { + self.webView?.sendEvent(name: "accelerometer_started", data: nil) + + if let refreshRate { + self.motionManager.accelerometerUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { data, error in + if let data = data { + let gravityConstant = 9.81 + self.webView?.sendEvent( + name: "accelerometer_changed", + data: "{x: \(data.acceleration.x * gravityConstant), y: \(data.acceleration.y * gravityConstant), z: \(data.acceleration.z * gravityConstant)}" + ) + } + } + } else { + if self.motionManager.isAccelerometerActive { + self.motionManager.stopAccelerometerUpdates() + } + self.webView?.sendEvent(name: "accelerometer_stopped", data: nil) + } + } + + private var isDeviceOrientationActive = false + fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isDeviceMotionAvailable else { + self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isDeviceOrientationActive != isActive else { + return + } + self.isDeviceOrientationActive = isActive + if isActive { + self.webView?.sendEvent(name: "device_orientation_started", data: nil) + + if let refreshRate { + self.motionManager.deviceMotionUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { data, error in + if let data { + self.webView?.sendEvent( + name: "device_orientation_changed", + data: "{alpha: \(data.attitude.roll), beta: \(data.attitude.pitch), gamma: \(data.attitude.yaw)}" + ) + } + } + } else { + if self.motionManager.isDeviceMotionActive { + self.motionManager.stopDeviceMotionUpdates() + } + self.webView?.sendEvent(name: "device_orientation_stopped", data: nil) + } + } + + private var isGyroscopeActive = false + fileprivate func setIsGyroscopeActive(_ isActive: Bool, refreshRate: Double? = nil) { + guard self.motionManager.isGyroAvailable else { + self.webView?.sendEvent(name: "gyroscope_failed", data: "{error: \"UNSUPPORTED\"}") + return + } + guard self.isGyroscopeActive != isActive else { + return + } + self.isGyroscopeActive = isActive + if isActive { + self.webView?.sendEvent(name: "gyroscope_started", data: nil) + + if let refreshRate { + self.motionManager.gyroUpdateInterval = refreshRate * 0.001 + } + self.motionManager.startGyroUpdates(to: OperationQueue.main) { data, error in + if let data { + self.webView?.sendEvent( + name: "gyroscope_changed", + data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}" + ) + } + } + } else { + if self.motionManager.isGyroActive { + self.motionManager.stopGyroUpdates() + } + self.webView?.sendEvent(name: "gyroscope_stopped", data: nil) + } + } + + fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) { + guard let controller = self.controller else { + return + } + + let botName = controller.botName + if let _ = expirationDate { + let _ = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + ).start(next: { [weak self] files, accountPeer, botPeer in + guard let self, let accountPeer, let controller = self.controller else { + return + } + guard let file = files[fileId] else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") + return + } + let confirmController = WebAppSetEmojiStatusScreen( + context: self.context, + botName: controller.botName, + accountPeer: accountPeer, + file: file, + completion: { [weak self, weak controller] result in + guard let self else { + return + } + if result { + let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.webView?.sendEvent(name: "emoji_status_set", data: nil) + }) + //TODO:localize + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller?.present(resultController, in: .window(.root)) + } else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") + } + } + ) + controller.parentController()?.push(confirmController) + }) + + + return + } + + let _ = combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)), + self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) + |> map { result -> [TelegramMediaFile] in + switch result { + case let .result(_, items, _): + return items.map(\.file) + default: + return [] + } + } + |> take(1) + ).start(next: { [weak self] files, accountPeer, botPeer, iconStatusEmoji in + guard let self, let accountPeer, let controller = self.controller else { + return + } + guard let file = files[fileId] else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}") + return + } + let alertController = webAppEmojiStatusAlertController( + context: self.context, + accountPeer: accountPeer, + botName: botName, + icons: iconStatusEmoji, + expirationDate: expirationDate, + completion: { [weak self, weak controller] result in + guard let self else { + return + } + if result { + let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate) + |> deliverOnMainQueue).start(completed: { [weak self] in + self?.webView?.sendEvent(name: "emoji_status_set", data: nil) + }) + //TODO:localize + if let botPeer { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller?.present(resultController, in: .window(.root)) + } + } else { + self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}") + } + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + + fileprivate func addToHomeScreen() { + guard let controller = self.controller else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + |> deliverOnMainQueue + ).start(next: { [weak controller] peer in + guard let peer, let addressName = peer.addressName else { + return + } + let encodedName = peer.compactDisplayTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedUsername = addressName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + + let url = URL(string: "http://64.225.73.234/?name=\(encodedName)&username=\(encodedUsername)")! + UIApplication.shared.open(url) + + controller?.dismiss() + }) + } + + fileprivate func checkLocation() { + guard let controller = self.controller else { + return + } + let _ = (webAppPermissionsState(context: self.context, peerId: controller.botId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + var data: [String: Any] = [:] + data["available"] = true + if let location = state?.location { + data["access_requested"] = location.isRequested + if location.isRequested { + data["access_granted"] = location.isAllowed + } + } else { + data["access_requested"] = false + } + if let serializedData = JSON(dictionary: data)?.string { + self.webView?.sendEvent(name: "location_checked", data: serializedData) + } + }) + } + + fileprivate func requestLocation() { + guard let controller = self.controller else { + return + } + let context = controller.context + let botId = controller.botId + let _ = (webAppPermissionsState(context: self.context, peerId: botId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] state in + guard let self else { + return + } + + var shouldRequest = false + if let location = state?.location { + if location.isRequested { + if location.isAllowed { + let locationCoordinates = Signal { subscriber in + return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in + subscriber.putNext(location) + subscriber.putCompletion() + }) + } |> deliverOnMainQueue + let _ = locationCoordinates.startStandalone(next: { location in + var data: [String: Any] = [:] + data["available"] = true + data["latitude"] = location.coordinate.latitude + data["longitude"] = location.coordinate.longitude + data["altitude"] = location.altitude + data["course"] = location.course + data["speed"] = location.speed + data["horizontal_accuracy"] = location.horizontalAccuracy + data["vertical_accuracy"] = location.verticalAccuracy + if #available(iOS 13.4, *) { + data["course_accuracy"] = location.courseAccuracy + } else { + data["course_accuracy"] = NSNull() + } + data["speed_accuracy"] = location.speedAccuracy + if let serializedData = JSON(dictionary: data)?.string { + self.webView?.sendEvent(name: "location_requested", data: serializedData) + } + }) + } else { + var data: [String: Any] = [:] + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } + } else { + shouldRequest = true + } + } else { + shouldRequest = true + } + + if shouldRequest { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: botId) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in + guard let accountPeer, let botPeer, let controller else { + return + } + let alertController = webAppLocationAlertController( + context: controller.context, + accountPeer: accountPeer, + botPeer: botPeer, + completion: { [weak self, weak controller] result in + guard let self, let controller else { + return + } + if result { + let resultController = UndoOverlayController( + presentationData: self.presentationData, + content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botPeer.compactDisplayTitle)** can now have access to your location.", action: "Undo", duration: 5.0), + elevatedLayout: true, + action: { action in + if case .undo = action { + + } + return true + } + ) + controller.present(resultController, in: .window(.root)) + + Queue.mainQueue().after(0.1, { + self.requestLocation() + }) + } else { + var data: [String: Any] = [:] + data["available"] = false + self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string) + } + let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in + return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result)) + }.start() + } + ) + controller.present(alertController, in: .window(.root)) + }) + } + }) + } } fileprivate var controllerNode: Node { @@ -1872,8 +2405,8 @@ public final class WebAppController: ViewController, AttachmentContainable { public let source: WebAppParameters.Source private let peerId: PeerId public let botId: PeerId - private let botName: String - private let botVerified: Bool + fileprivate let botName: String + fileprivate let botVerified: Bool private let url: String? private let queryId: Int64? private let payload: String? @@ -1882,6 +2415,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let keepAliveSignal: Signal? private let replyToMessageId: MessageId? private let threadId: Int64? + public var isFullscreen: Bool private var presentationData: PresentationData fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? @@ -1909,6 +2443,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.keepAliveSignal = params.keepAliveSignal self.replyToMessageId = replyToMessageId self.threadId = threadId + self.isFullscreen = params.isFullscreen self.updatedPresentationData = updatedPresentationData @@ -1927,22 +2462,22 @@ public final class WebAppController: ViewController, AttachmentContainable { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style -// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) - self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) - self.navigationItem.leftBarButtonItem?.target = self - - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) - self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) - self.navigationItem.rightBarButtonItem?.target = self - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - - let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme) - titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified) - self.navigationItem.titleView = titleView - self.titleView = titleView + if !self.isFullscreen { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) + self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) + self.navigationItem.leftBarButtonItem?.target = self + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) + self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) + self.navigationItem.rightBarButtonItem?.target = self + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme) + titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified) + self.navigationItem.titleView = titleView + self.titleView = titleView + } self.moreButtonNode.action = { [weak self] _, gesture in if let strongSelf = self { @@ -2024,7 +2559,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationBar?.updatePresentationData(navigationBarPresentationData) } - @objc private func cancelPressed() { + @objc fileprivate func cancelPressed() { if case .back = self.cancelButtonNode.state { self.controllerNode.sendBackButtonEvent() } else { @@ -2034,11 +2569,14 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - @objc private func moreButtonPressed() { + @objc fileprivate func moreButtonPressed() { self.moreButtonNode.buttonPressed() } - @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { + @objc fileprivate func morePressed(node: ASDisplayNode, gesture: ContextGesture?) { + guard let node = node as? ContextReferenceContentNode else { + return + } let context = self.context var presentationData = self.presentationData if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor { @@ -2170,7 +2708,18 @@ public final class WebAppController: ViewController, AttachmentContainable { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_PrivacyPolicy_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {}) } }))) - + + #if DEBUG + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Add to Home Screen", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + self?.controllerNode.addToHomeScreen() + }))) + #endif + if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) @@ -2267,6 +2816,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.requestLayout(transition: .immediate) self.controllerNode.webView?.setNeedsLayout() } + + self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \"\(self.isMinimized ? "false" : "true")\"}") } } } @@ -2275,6 +2826,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return true } + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + (self.parentController() as? AttachmentController)?.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: initialVelocity) + } + public func shouldDismissImmediately() -> Bool { if self.controllerNode.needDismissConfirmation { return false diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift new file mode 100644 index 0000000000..9bbdd20916 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -0,0 +1,365 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import EmojiTextAttachmentView +import TextFormat +import Markdown + +private final class IconsNode: ASDisplayNode { + private let context: AccountContext + private var animationLayer: InlineStickerItemLayer? + + private var files: [TelegramMediaFile] + private var currentIndex = 0 + private var switchingToNext = false + + private var timer: SwiftSignalKit.Timer? + + private var currentParams: (size: CGSize, theme: PresentationTheme)? + + init(context: AccountContext, files: [TelegramMediaFile]) { + self.context = context + self.files = files + + super.init() + } + + deinit { + self.timer?.invalidate() + } + + func updateLayout(size: CGSize, theme: PresentationTheme) { + self.currentParams = (size, theme) + + if self.timer == nil { + self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.switchingToNext = true + if let (size, theme) = self.currentParams { + self.updateLayout(size: size, theme: theme) + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + let animationLayer: InlineStickerItemLayer + var disappearingAnimationLayer: InlineStickerItemLayer? + if let current = self.animationLayer, !self.switchingToNext { + animationLayer = current + } else { + if self.switchingToNext { + self.currentIndex = (self.currentIndex + 1) % self.files.count + disappearingAnimationLayer = self.animationLayer + } + let file = self.files[self.currentIndex] + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file.fileId.id, + file: file + ) + animationLayer = InlineStickerItemLayer( + context: .account(self.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + unique: true, + placeholderColor: theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: 20.0, height: 20.0), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + animationLayer.dynamicColor = theme.actionSheet.controlAccentColor + self.view.layer.addSublayer(animationLayer) + self.animationLayer = animationLayer + + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) + animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + + animationLayer.frame = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 20.0)) + + if let disappearingAnimationLayer { + disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + disappearingAnimationLayer.removeFromSuperlayer() + }) + disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true) + disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } +} + +private final class WebAppEmojiStatusAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let presentationTheme: PresentationTheme + private let botName: String + + private let textNode: ASTextNode + private let iconBackgroundNode: ASImageNode + private let iconAvatarNode: AvatarNode + private let iconNameNode: ASTextNode + private let iconAnimationNode: IconsNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + accountPeer: EnginePeer, + botName: String, + icons: [TelegramMediaFile], + actions: [TextAlertAction] + ) { + self.strings = strings + self.presentationTheme = ptheme + self.botName = botName + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.iconBackgroundNode = ASImageNode() + self.iconBackgroundNode.displaysAsynchronously = false + self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor) + + self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) + + self.iconNameNode = ASTextNode() + self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor) + + self.iconAnimationNode = IconsNode(context: context, files: icons) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.iconBackgroundNode) + self.addSubnode(self.iconAvatarNode) + self.addSubnode(self.iconNameNode) + self.addSubnode(self.iconAnimationNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + //TODO:localize + let string = "**\(self.botName)** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **\(self.botName)**." + let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.textNode.attributedText = attributedText + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width , 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let iconSpacing: CGFloat = 6.0 + let iconSize = CGSize(width: 32.0, height: 32.0) + let nameSize = self.iconNameNode.measure(size) + let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width + + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height)) + transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame) + transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0)) + transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize)) + + self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme) + self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0) + + origin.y += iconSize.height + 16.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = minActionsWidth + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func webAppEmojiStatusAlertController( + context: AccountContext, + accountPeer: EnginePeer, + botName: String, + icons: [TelegramMediaFile], + expirationDate: Int32?, + completion: @escaping (Bool) -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + let strings = presentationData.strings + + var dismissImpl: ((Bool) -> Void)? + var contentNode: WebAppEmojiStatusAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + dismissImpl?(true) + + completion(false) + }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + dismissImpl?(true) + + completion(true) + })] + + contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/WebUI/Sources/WebAppLocationAlertController.swift b/submodules/WebUI/Sources/WebAppLocationAlertController.swift new file mode 100644 index 0000000000..9f4500321a --- /dev/null +++ b/submodules/WebUI/Sources/WebAppLocationAlertController.swift @@ -0,0 +1,293 @@ + +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown +import CheckNode + +private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { + let size = CGSize(width: 28.0, height: 28.0) + return generateImage(size, contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.addEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [UIColor(rgb: 0x36c089).cgColor, UIColor(rgb: 0x3ca5eb).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: bounds.insetBy(dx: 6.0, dy: 6.0)) + } + + context.resetClip() + + let lineWidth = 2.0 - UIScreenPixel + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) + }, opaque: false) +} + +private final class WebAppLocationAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let text: String + + private let textNode: ASTextNode + private let avatarNode: AvatarNode + private let arrowNode: ASImageNode + private let secondAvatarNode: AvatarNode + private let iconNode: ASImageNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, accountPeer: EnginePeer, botPeer: EnginePeer, text: String, actions: [TextAlertAction]) { + self.strings = strings + self.text = text + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateBoostIcon(theme: ptheme) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.secondAvatarNode) + self.addSubnode(self.iconNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) + self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: botPeer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) + + if let icon = self.iconNode.image { + let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + } + + origin.y += avatarSize.height + 10.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + //TODO:localize + let text = "**\(botPeer.compactDisplayTitle)** requests access to your **location**. You will be able to revoke this access in the profile page of **\(botPeer.compactDisplayTitle)**." + + var dismissImpl: ((Bool) -> Void)? + var contentNode: WebAppLocationAlertContentNode? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: { + dismissImpl?(true) + completion(false) + }), TextAlertAction(type: .defaultAction, title: "Allow", action: { + dismissImpl?(true) + completion(true) + })] + + contentNode = WebAppLocationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, accountPeer: accountPeer, botPeer: botPeer, text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift new file mode 100644 index 0000000000..18297b55ef --- /dev/null +++ b/submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift @@ -0,0 +1,454 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode +import ListItemComponentAdaptor +import ChatMessageItemImpl + +final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + struct MessageItem: Equatable { + static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool { + if lhs.outgoing != rhs.outgoing { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.author != rhs.author { + return false + } + if lhs.photo != rhs.photo { + return false + } + if lhs.nameColor != rhs.nameColor { + return false + } + if lhs.backgroundEmojiId != rhs.backgroundEmojiId { + return false + } + if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 || lhsReply.2 != rhsReply.2 { + return false + } else if (lhs.reply == nil) != (rhs.reply == nil) { + return false + } + if let lhsLinkPreview = lhs.linkPreview, let rhsLinkPreview = rhs.linkPreview, lhsLinkPreview.0 != rhsLinkPreview.0 || lhsLinkPreview.1 != rhsLinkPreview.1 || lhsLinkPreview.2 != rhsLinkPreview.2 { + return false + } else if (lhs.linkPreview == nil) != (rhs.linkPreview == nil) { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + let outgoing: Bool + let peerId: EnginePeer.Id + let author: String + let photo: [TelegramMediaImageRepresentation] + let nameColor: PeerNameColor + let backgroundEmojiId: Int64? + let reply: (String, String, PeerNameColor)? + let linkPreview: (String, String, String)? + let text: String + } + + let context: AccountContext + let theme: PresentationTheme + let componentTheme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let messageItems: [MessageItem] + + init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) { + self.context = context + self.theme = theme + self.componentTheme = componentTheme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.messageItems = messageItems + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = PeerNameColorChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? PeerNameColorChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.componentTheme !== rhs.componentTheme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.chatBubbleCorners != rhs.chatBubbleCorners { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.messageItems != rhs.messageItems { + return false + } + return true + } +} + +final class PeerNameColorChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private let containerNode: ASDisplayNode + private var messageNodes: [ListViewItemNode]? + private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:] + + private var item: PeerNameColorChatPreviewItem? + + private let disposable = MetaDisposable() + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + self.addSubnode(self.containerNode) + } + + deinit { + self.disposable.dispose() + } + + func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNodes = self.messageNodes + + var currentBackgroundNode = self.backgroundNode + + let currentItem = self.item + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners) + } + + var insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)) + + var items: [ListViewItem] = [] + for messageItem in item.messageItems.reversed() { + let authorPeerId = messageItem.peerId + let replyAuthorPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(10)) + + var peers = SimpleDictionary() + var messages = SimpleDictionary() + + peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + + + let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3) + if let (replyAuthor, text, replyColor) = messageItem.reply { + peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) + + messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + } + + var media: [Media] = [] + if let (site, title, text) = messageItem.linkPreview, params.width > 320.0 { + media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))) + } + + var attributes: [MessageAttribute] = [] + if messageItem.reply != nil { + attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) + } + + attributes.append(InlineBotMessageAttribute(peerId: nil, title: "Test Attach")) + + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: attributes, media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) + } + + var nodes: [ListViewItemNode] = [] + if let messageNodes = currentNodes { + nodes = messageNodes + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + Queue.mainQueue().after(0.01) { + apply(ListViewItemApply(isOnScreen: true)) + } + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + } + nodes = messageNodes + } + + var contentSize = CGSize(width: params.width, height: 4.0 + 4.0) + for node in nodes { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + if params.width <= 320.0 { + insets.top = 0.0 + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let leftInset = params.leftInset + let rightInset = params.leftInset + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let currentBackgroundNode { + currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false) + currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + } + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId || currentItem.theme !== item.theme || currentItem.wallpaper != item.wallpaper { + if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { + snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) + strongSelf.view.addSubview(snapshot) + snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in + snapshot.removeFromSuperview() + }) + } + } + + strongSelf.messageNodes = nodes + var topOffset: CGFloat = 4.0 + for node in nodes { + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize) + topOffset += node.frame.size.height + + if let header = node.headers()?.first(where: { $0 is ChatMessageAvatarHeader }) { + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 3.0 + node.frame.minY), size: CGSize(width: layoutSize.width, height: header.height)) + let stickLocationDistanceFactor: CGFloat = 0.0 + + let id = header.id + let headerNode: ListViewItemHeaderNode + if let current = strongSelf.itemHeaderNodes[id] { + headerNode = current + headerNode.updateFrame(headerFrame, within: layoutSize) + + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } else { + headerNode = header.node(synchronousLoad: true) + if headerNode.item !== header { + header.updateNode(headerNode, previous: nil, next: nil) + headerNode.item = header + } + headerNode.frame = headerFrame + headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset) + strongSelf.itemHeaderNodes[id] = headerNode + + strongSelf.containerNode.addSubnode(headerNode) + headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate) + } + } + } + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + if params.isStandalone { + strongSelf.topStripeNode.isHidden = true + strongSelf.bottomStripeNode.isHidden = true + strongSelf.maskNode.isHidden = true + } else { + let hasCorners = itemListHasRoundedBlockLayout(params) + + var hasTopCorners = false + var hasBottomCorners = false + + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + + let displayMode: WallpaperDisplayMode + if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 { + displayMode = .halfAspectFill + } else { + if backgroundFrame.width > backgroundFrame.height * 4.0 { + if params.availableHeight < 700.0 { + displayMode = .halfAspectFill + } else { + displayMode = .aspectFill + } + } else { + displayMode = .aspectFill + } + } + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame + backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate) + } + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift new file mode 100644 index 0000000000..39af0f6031 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -0,0 +1,337 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import ListItemComponentAdaptor +import TelegramStringFormatting + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let dismiss: () -> Void + + init( + context: AccountContext, + dismiss: @escaping () -> Void + ) { + self.context = context + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let closeButton = Child(Button.self) + let title = Child(Text.self) + let amountSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + + let controller = environment.controller + + let theme = environment.theme.withModalBlocksBackground() + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 120.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: closeButton.size.width / 2.0 + sideInset, y: 28.0)) + ) + + let title = title.update( + component: Text(text: "Share Message", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 40.0 + + let amountFont = Font.regular(13.0) + let amountTextColor = theme.list.freeTextColor + let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Test Attach mini app suggests you to send this message to a chat you select.", attributes: amountMarkdownAttributes, textAlignment: .natural)) + let amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + if let controller = controller() as? WebAppMessagePreviewScreen, let navigationController = controller.navigationController as? NavigationController { + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } + )) + + let messageItem = PeerNameColorChatPreviewItem.MessageItem( + outgoing: true, + peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), + author: "", + photo: [], + nameColor: .blue, + backgroundEmojiId: nil, + reply: nil, + linkPreview: nil, + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" + ) + + let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + + let amountSection = amountSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Message Preview".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: amountFooter, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: PeerNameColorChatPreviewItem( + context: component.context, + theme: environment.theme, + componentTheme: environment.theme, + strings: environment.strings, + sectionId: 0, + fontSize: presentationData.chatFontSize, + chatBubbleCorners: presentationData.chatBubbleCorners, + wallpaper: presentationData.chatWallpaper, + dateTimeFormat: environment.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + messageItems: [messageItem] + ), + params: listItemParams + ))) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += amountSection.size.height + contentSize.height += 32.0 + + let buttonString: String = "Share With..." + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { + if let controller = controller() as? WebAppMessagePreviewScreen { + let _ = controller + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 15.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } + + final class State: ComponentState { + var cachedCloseImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } +} + +private final class WebAppMessagePreviewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: WebAppMessagePreviewSheetComponent, rhs: WebAppMessagePreviewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.list.blocksBackgroundColor), + followContentSizeChanges: false, + clipsContent: true, + isScrollEnabled: false, + animateOut: animateOut + ), + 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(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let completion: (Bool) -> Void + + public init( + context: AccountContext, + completion: @escaping (Bool) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: WebAppMessagePreviewSheetComponent( + context: context + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + 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() + } + } +} diff --git a/submodules/WebUI/Sources/WebAppPermissions.swift b/submodules/WebUI/Sources/WebAppPermissions.swift new file mode 100644 index 0000000000..2a53edab6e --- /dev/null +++ b/submodules/WebUI/Sources/WebAppPermissions.swift @@ -0,0 +1,98 @@ +import Foundation +import NaturalLanguage +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramUIPreferences + +public struct WebAppPermissionsState: Codable { + enum CodingKeys: String, CodingKey { + case location + } + + public struct Location: Codable { + enum CodingKeys: String, CodingKey { + case isRequested + case isAllowed + } + + public let isRequested: Bool + public let isAllowed: Bool + + public init( + isRequested: Bool, + isAllowed: Bool + ) { + self.isRequested = isRequested + self.isAllowed = isAllowed + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.isRequested = try container.decode(Bool.self, forKey: .isRequested) + self.isAllowed = try container.decode(Bool.self, forKey: .isAllowed) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.isRequested, forKey: .isRequested) + try container.encode(self.isAllowed, forKey: .isAllowed) + } + } + + public let location: Location? + + public init( + location: Location? + ) { + self.location = location + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.location = try container.decode(WebAppPermissionsState.Location.self, forKey: .location) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(self.location, forKey: .location) + } +} + +public func webAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + return context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)) + |> map { entry -> WebAppPermissionsState? in + return entry?.get(WebAppPermissionsState.self) + } +} + +private func updateWebAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id, state: WebAppPermissionsState?) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + if let state { + return context.engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key, item: state) + } else { + return context.engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key) + } +} + +public func updateWebAppPermissionsStateInteractively(context: AccountContext, peerId: EnginePeer.Id, _ f: @escaping (WebAppPermissionsState?) -> WebAppPermissionsState?) -> Signal { + let key = EngineDataBuffer(length: 8) + key.setInt64(0, value: peerId.id._internalGetInt64Value()) + + return context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)) + |> map { entry -> WebAppPermissionsState? in + return entry?.get(WebAppPermissionsState.self) + } + |> mapToSignal { current -> Signal in + return updateWebAppPermissionsState(context: context, peerId: peerId, state: f(current)) + } +} diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift new file mode 100644 index 0000000000..db35fbc387 --- /dev/null +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -0,0 +1,390 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import AccountContext +import PresentationDataUtils +import PremiumPeerShortcutComponent +import GiftAnimationComponent + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let botName: String + let accountPeer: EnginePeer + let file: TelegramMediaFile + let dismiss: () -> Void + + init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile, + dismiss: @escaping () -> Void + ) { + self.context = context + self.botName = botName + self.accountPeer = accountPeer + self.file = file + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.botName != rhs.botName { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + final class State: ComponentState { + var cachedCloseImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let animation = Child(GiftAnimationComponent.self) + let closeButton = Child(Button.self) + let title = Child(Text.self) + let text = Child(BalancedTextComponent.self) + + let peerShortcut = Child(PremiumPeerShortcutComponent.self) + let button = Child(ButtonComponent.self) + + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, 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 animation = animation.update( + component: GiftAnimationComponent( + context: component.context, + theme: environment.theme, + file: component.file + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0)) + ) + + let closeImage: UIImage + if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + contentSize.height += 128.0 + + let title = title.update( + component: Text(text: "Set Emoji Status", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 13.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let text = text.update( + component: BalancedTextComponent( + text: .markdown( + text: "Do you want to set this emoji status suggested by **\(component.botName)**?", + attributes: markdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) + ) + contentSize.height += text.size.height + contentSize.height += 15.0 + + let peerShortcut = peerShortcut.update( + component: PremiumPeerShortcutComponent( + context: component.context, + theme: theme, + peer: component.accountPeer + ), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(peerShortcut + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0)) + ) + contentSize.height += peerShortcut.size.height + contentSize.height += 32.0 + + let controller = environment.controller() as? WebAppSetEmojiStatusScreen + + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Confirm", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak controller] in + controller?.complete(result: true) + controller?.dismissAnimated() + } + ), + availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + + contentSize.height += 48.0 + + return contentSize + } + } +} + +private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let botName: String + private let accountPeer: EnginePeer + private let file: TelegramMediaFile + + init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile + ) { + self.context = context + self.botName = botName + self.accountPeer = accountPeer + self.file = file + } + + static func ==(lhs: WebAppSetEmojiStatusSheetComponent, rhs: WebAppSetEmojiStatusSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.botName != rhs.botName { + return false + } + if lhs.accountPeer != rhs.accountPeer { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + botName: context.component.botName, + accountPeer: context.component.accountPeer, + file: context.component.file, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + 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(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class WebAppSetEmojiStatusScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let completion: (Bool) -> Void + + public init( + context: AccountContext, + botName: String, + accountPeer: EnginePeer, + file: TelegramMediaFile, + completion: @escaping (Bool) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: WebAppSetEmojiStatusSheetComponent( + context: context, + botName: botName, + accountPeer: accountPeer, + file: file + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var didComplete = false + fileprivate func complete(result: Bool) { + guard !self.didComplete else { + return + } + self.didComplete = true + self.completion(result) + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 20956490af..f1be891cc9 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -92,24 +92,16 @@ function tgBrowserDisconnectObserver() { final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } - var customSideInset: CGFloat = 0.0 { + var customInsets: UIEdgeInsets = .zero { didSet { - if self.customSideInset != oldValue { + if self.customInsets != oldValue { self.setNeedsLayout() } } } - - var customBottomInset: CGFloat = 0.0 { - didSet { - if self.customBottomInset != oldValue { - self.setNeedsLayout() - } - } - } - + override var safeAreaInsets: UIEdgeInsets { - return UIEdgeInsets(top: 0.0, left: self.customSideInset, bottom: self.customBottomInset, right: self.customSideInset) + return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right) } init(account: Account) { @@ -241,8 +233,11 @@ final class WebAppWebView: WKWebView { } func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition) { - let data = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}" - self.sendEvent(name: "viewport_changed", data: data) + let viewportData = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}" + self.sendEvent(name: "viewport_changed", data: viewportData) + + let safeInsetsData = "{top:\(self.customInsets.top), bottom:\(self.customInsets.bottom), left:\(self.customInsets.left), right:\(self.customInsets.right)}" + self.sendEvent(name: "safe_area_changed", data: safeInsetsData) } var lastTouchTimestamp: Double?