diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2993d91058..6e466ac255 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12602,3 +12602,47 @@ Sorry for the inconvenience."; "Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile."; "Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile."; "Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile."; + +"ChatList.Search.FilterApps" = "Apps"; +"ChatList.Search.SectionPopularApps" = "POPULAR APPS"; +"ChatList.Search.SectionRecentApps" = "APPS YOU USE"; +"ChatList.Search.Apps.Empty.Text" = "No Apps Found"; + +"VoiceChat.MicrophoneModes" = "Microphone Modes"; + +"MiniAppList.Title" = "Examples"; +"MiniAppList.ListSectionHeader" = "APPS THAT ACCEPT STARS"; + +"PeerInfo.PaneBotPreviews" = "Preview"; +"PeerInfo.OpenAppButton" = "Open App"; +"PeerInfo.AppFooterAdmin" = "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy)."; +"PeerInfo.AppFooter" = "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy)."; +"BotPreviews.MenuAddPreview" = "Add Preview"; +"BotPreviews.MenuReorder" = "Reorder"; +"BotPreviews.MenuDeleteLanguage" = "Delete %@"; +"BotPreviews.SubtitleLoading" = "loading"; +"BotPreviews.SubtitleEmpty" = "no preview added"; +"BotPreviews.SubtitleCount_1" = "1 preview"; +"BotPreviews.SubtitleCount_any" = "1 previews"; +"BotPreviews.SheetDeleteTitle_1" = "Delete 1 Preview?"; +"BotPreviews.SheetDeleteTitle_any" = "Delete %d Previews?"; +"BotPreviews.LanguageTab.Main" = "Main"; +"BotPreviews.LanguageTab.Add" = "+ Add Language"; +"BotPreviews.Empty.Title" = "No Preview"; +"BotPreviews.Empty.Text_1" = "Upload up to 1 screenshot and video demos for your mini app."; +"BotPreviews.Empty.Text_any" = "Upload up to %d screenshots and video demos for your mini app."; +"BotPreviews.Empty.Add" = "Add Preview"; +"BotPreviews.Empty.AddTranslation" = "Create a Translation"; +"BotPreviews.Empty.DeleteTranslation" = "Delete this Translation"; +"BotPreviews.Empty.Separator" = "or"; +"BotPreviews.AlertTooManyPreviews_1" = "You can add at most 1 preview."; +"BotPreviews.AlertTooManyPreviews_any" = "You can add at most 1 previews."; +"BotPreviews.DeleteTranslationAlert.Title" = "Delete Translation"; +"BotPreviews.DeleteTranslationAlert.Text" = "Are you sure you want to delete this translation?"; +"BotPreviews.TranslationFooter.Text" = "This preview will be displayed for all users who have %@ set as their language."; +"BotPreviews.DefaultFooter.Text" = "This preview will be shown by default. You can also add translations into specific languages."; +"BotPreviews.SelectLanguage.Title" = "Add a Translation"; +"BotPreview.ViewContextDelete" = "Delete Preview"; + +"WebBrowser.Download.Confirmation" = "Do you want to download \"%@\"?"; +"WebBrowser.Download.Download" = "Download"; diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index d143a87202..f03e31beb4 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode", "//submodules/SearchUI", "//submodules/SearchBarNode", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index 679f3ea5c3..cb5f0d373c 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -15,6 +15,7 @@ final class AddressBarContentComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let metrics: LayoutMetrics let url: String let isSecure: Bool let isExpanded: Bool @@ -23,6 +24,7 @@ final class AddressBarContentComponent: Component { init( theme: PresentationTheme, strings: PresentationStrings, + metrics: LayoutMetrics, url: String, isSecure: Bool, isExpanded: Bool, @@ -30,6 +32,7 @@ final class AddressBarContentComponent: Component { ) { self.theme = theme self.strings = strings + self.metrics = metrics self.url = url self.isSecure = isSecure self.isExpanded = isExpanded @@ -43,6 +46,9 @@ final class AddressBarContentComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.url != rhs.url { return false } @@ -78,6 +84,7 @@ final class AddressBarContentComponent: Component { var title: String var isSecure: Bool var collapseFraction: CGFloat + var isTablet: Bool static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { @@ -101,6 +108,9 @@ final class AddressBarContentComponent: Component { if lhs.collapseFraction != rhs.collapseFraction { return false } + if lhs.isTablet != rhs.isTablet { + return false + } return true } } @@ -206,9 +216,9 @@ final class AddressBarContentComponent: Component { self.activated(true) if let textField = self.textField { textField.becomeFirstResponder() - Queue.mainQueue().justDispatch { + Queue.mainQueue().after(0.3, { textField.selectAll(nil) - } + }) } } @@ -238,7 +248,9 @@ final class AddressBarContentComponent: Component { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let component = self.component { - component.performAction.invoke(.navigateTo(explicitUrl(textField.text ?? ""))) + let finalUrl = explicitUrl(textField.text ?? "") +// finalUrl = finalUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? finalUrl + component.performAction.invoke(.navigateTo(finalUrl)) } textField.endEditing(true) return false @@ -252,7 +264,7 @@ final class AddressBarContentComponent: Component { self.placeholderContent.view?.isHidden = !text.isEmpty if let params = self.params { - self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, transition: .immediate) + self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, isTablet: params.isTablet, transition: .immediate) } } @@ -273,13 +285,18 @@ final class AddressBarContentComponent: Component { var title: String = "" if let parsedUrl = URL(string: component.url) { title = parsedUrl.host ?? component.url + if title.hasPrefix("www.") { + title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4)) + } + title = title.idnaDecoded ?? title } - self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition) + + self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, isTablet: component.metrics.isTablet, transition: transition) return availableSize } - public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, transition: ComponentTransition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, isTablet: Bool, transition: ComponentTransition) { let params = Params( theme: theme, strings: strings, @@ -287,7 +304,8 @@ final class AddressBarContentComponent: Component { isActive: isActive, title: title, isSecure: isSecure, - collapseFraction: collapseFraction + collapseFraction: collapseFraction, + isTablet: isTablet ) if self.params == params { @@ -327,12 +345,13 @@ final class AddressBarContentComponent: Component { let cancelButtonSpacing: CGFloat = 8.0 var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) - if isActiveWithText { + if isActiveWithText && !isTablet { backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing } transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) + self.cancelButton.isUserInteractionEnabled = isActiveWithText && !isTablet let textX: CGFloat = backgroundFrame.minX + sideInset let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) @@ -410,7 +429,7 @@ final class AddressBarContentComponent: Component { cancelButtonTitleComponentView.isUserInteractionEnabled = false } transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) - transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0) + transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText && !isTablet ? 1.0 : 0.0) } let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) @@ -431,7 +450,25 @@ final class AddressBarContentComponent: Component { textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) } - textField.text = self.component?.url ?? "" + var address = self.component?.url ?? "" + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + + if textField.text != address { + textField.text = address + self.clearIconView.isHidden = address.isEmpty + self.clearIconButton.isHidden = address.isEmpty + self.placeholderContent.view?.isHidden = !address.isEmpty + } textField.textColor = theme.rootController.navigationSearchBar.inputTextColor transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideInset - 32.0, height: backgroundFrame.height))) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index b33621c4ee..2070587445 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -13,17 +13,20 @@ final class BrowserAddressListComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings + let insets: UIEdgeInsets let navigateTo: (String) -> Void init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + insets: UIEdgeInsets, navigateTo: @escaping (String) -> Void ) { self.context = context self.theme = theme self.strings = strings + self.insets = insets self.navigateTo = navigateTo } @@ -37,6 +40,9 @@ final class BrowserAddressListComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.insets != rhs.insets { + return false + } return true } @@ -148,7 +154,7 @@ final class BrowserAddressListComponent: Component { } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - self.endEditing(true) + self.window?.endEditing(true) } private func updateScrolling(transition: ComponentTransition) { @@ -211,6 +217,7 @@ final class BrowserAddressListComponent: Component { theme: component.theme, style: .plain, title: sectionTitle, + insets: component.insets, actionTitle: section.id == 0 ? "Clear" : nil, action: { [weak self] in if let self, let component = self.component { @@ -292,6 +299,7 @@ final class BrowserAddressListComponent: Component { webPage: webPage!, message: itemMessage, hasNext: true, + insets: component.insets, action: { if let url = webPage?.content.url { navigateTo(url) @@ -393,6 +401,7 @@ final class BrowserAddressListComponent: Component { webPage: TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))), message: nil, hasNext: true, + insets: .zero, action: {} )), environment: {}, diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 49ebcb2c20..30ad83d58b 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -16,6 +16,7 @@ final class BrowserAddressListItemComponent: Component { let webPage: TelegramMediaWebpage var message: Message? let hasNext: Bool + let insets: UIEdgeInsets let action: () -> Void init( @@ -24,6 +25,7 @@ final class BrowserAddressListItemComponent: Component { webPage: TelegramMediaWebpage, message: Message?, hasNext: Bool, + insets: UIEdgeInsets, action: @escaping () -> Void ) { self.context = context @@ -31,6 +33,7 @@ final class BrowserAddressListItemComponent: Component { self.webPage = webPage self.message = message self.hasNext = hasNext + self.insets = insets self.action = action } @@ -44,6 +47,9 @@ final class BrowserAddressListItemComponent: Component { if lhs.hasNext != rhs.hasNext { return false } + if lhs.insets != rhs.insets { + return false + } return true } @@ -92,7 +98,7 @@ final class BrowserAddressListItemComponent: Component { let iconSize = CGSize(width: 40.0, height: 40.0) let height: CGFloat = 60.0 - let leftInset: CGFloat = 11.0 + iconSize.width + 11.0 + let leftInset: CGFloat = component.insets.left + 11.0 + iconSize.width + 11.0 let rightInset: CGFloat = 16.0 let titleSpacing: CGFloat = 2.0 @@ -181,7 +187,7 @@ final class BrowserAddressListItemComponent: Component { } - let iconFrame = CGRect(origin: CGPoint(x: 11.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: 11.0 + component.insets.left, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) let iconImageLayout = self.icon.asyncLayout() var iconImageApply: (() -> Void)? diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 7096b83042..7422bed87e 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -162,12 +162,14 @@ protocol BrowserContent: UIView { var present: (ViewController, Any?) -> Void { get set } var presentInGlobalOverlay: (ViewController) -> Void { get set } var getNavigationController: () -> NavigationController? { get set } + var openAppUrl: (String) -> Void { get set } var minimize: () -> Void { get set } var close: () -> Void { get set } var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set } - + func resetScrolling() + func reload() func stop() @@ -186,7 +188,7 @@ protocol BrowserContent: UIView { func addToRecentlyVisited() - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) func makeContentSnapshotView() -> UIView? } diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 26b1d11e7f..316ab28538 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -17,7 +17,6 @@ import ShareController import UndoUI import UrlEscaping - final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -37,6 +36,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -101,7 +101,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) } } @@ -240,7 +240,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) @@ -360,6 +360,10 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { // self.currentError = nil self.updateFontState(self.currentFontState, force: true) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 29bed73a96..2ceff5ec34 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -66,6 +66,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -300,7 +301,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg guard let (size, insets, fullInsets) = self.containerLayout else { return } - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: transition) } func reload() { @@ -374,11 +375,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition.containedViewLayoutTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: transition.containedViewLayoutTransition) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.containerLayout = (size, insets, fullInsets) var updateVisibleItems = false @@ -766,6 +767,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.readingProgress.set(readingProgress) } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { var contentOffset = CGPoint() for (_, itemNode) in self.visibleItemsWithNodes { diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index d31d7fd6a1..674b5a7b9c 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -29,12 +29,14 @@ final class BrowserNavigationBarComponent: CombinedComponent { let topInset: CGFloat let height: CGFloat let sideInset: CGFloat + let metrics: LayoutMetrics let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] let centerItem: AnyComponentWithIdentity? let readingProgress: CGFloat let loadingProgress: Double? let collapseFraction: CGFloat + let activate: () -> Void init( backgroundColor: UIColor, @@ -45,12 +47,14 @@ final class BrowserNavigationBarComponent: CombinedComponent { topInset: CGFloat, height: CGFloat, sideInset: CGFloat, + metrics: LayoutMetrics, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], centerItem: AnyComponentWithIdentity?, readingProgress: CGFloat, loadingProgress: Double?, - collapseFraction: CGFloat + collapseFraction: CGFloat, + activate: @escaping () -> Void ) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor @@ -60,12 +64,14 @@ final class BrowserNavigationBarComponent: CombinedComponent { self.topInset = topInset self.height = height self.sideInset = sideInset + self.metrics = metrics self.leftItems = leftItems self.rightItems = rightItems self.centerItem = centerItem self.readingProgress = readingProgress self.loadingProgress = loadingProgress self.collapseFraction = collapseFraction + self.activate = activate } static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool { @@ -93,6 +99,9 @@ final class BrowserNavigationBarComponent: CombinedComponent { if lhs.sideInset != rhs.sideInset { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.leftItems != rhs.leftItems { return false } @@ -122,6 +131,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self) + let activate = Child(Button.self) return { context in var availableWidth = context.availableSize.width @@ -131,6 +141,8 @@ final class BrowserNavigationBarComponent: CombinedComponent { let expandedHeight = context.component.height let contentHeight: CGFloat = expandedHeight * (1.0 - context.component.collapseFraction) + collapsedHeight * context.component.collapseFraction let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) + let verticalOffset: CGFloat = context.component.metrics.isTablet ? -3.0 : 0.0 + let itemSpacing: CGFloat = context.component.metrics.isTablet ? 24.0 : 8.0 let background = background.update( component: Rectangle(color: context.component.backgroundColor.withAlphaComponent(1.0)), @@ -160,7 +172,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { availableSize: CGSize(width: size.width, height: size.height), transition: context.transition ) - + var leftItemList: [_UpdatedChildComponent] = [] for item in context.component.leftItems { let item = leftItems[item.id].update( @@ -182,11 +194,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { rightItemList.append(item) availableWidth -= item.size.width } - - if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 14.0 - } - + context.add(background .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) ) @@ -212,35 +220,37 @@ final class BrowserNavigationBarComponent: CombinedComponent { var leftItemX = sideInset for item in leftItemList { context.add(item - .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .opacity(1.0 - context.component.collapseFraction) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - leftItemX += item.size.width + 8.0 - centerLeftInset += item.size.width + 8.0 + leftItemX += item.size.width + itemSpacing + centerLeftInset += item.size.width + itemSpacing } var centerRightInset = sideInset - 5.0 var rightItemX = context.availableSize.width - (sideInset - 5.0) for item in rightItemList.reversed() { context.add(item - .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .opacity(1.0 - context.component.collapseFraction) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - rightItemX -= item.size.width + 8.0 - centerRightInset += item.size.width + 8.0 + rightItemX -= item.size.width + itemSpacing + centerRightInset += item.size.width + itemSpacing } let maxCenterInset = max(centerLeftInset, centerRightInset) if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 20.0 + availableWidth -= itemSpacing * CGFloat(max(0, leftItemList.count - 1)) + itemSpacing * CGFloat(max(0, rightItemList.count - 1)) + 30.0 } + availableWidth -= context.component.sideInset * 2.0 + availableWidth = min(660.0, availableWidth) let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) @@ -259,13 +269,30 @@ final class BrowserNavigationBarComponent: CombinedComponent { } if let centerItem = centerItem { context.add(centerItem - .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) } + if context.component.collapseFraction == 1.0 { + let activateAction = context.component.activate + let activate = activate.update( + component: Button( + content: AnyComponent(Rectangle(color: UIColor(rgb: 0x000000, alpha: 0.001))), + action: { + activateAction() + } + ), + availableSize: size, + transition: .immediate + ) + context.add(activate + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + } + return size } } diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 3bd36c1cf6..cb61825774 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -38,6 +38,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -110,7 +111,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor } if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) } } @@ -249,7 +250,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating) @@ -352,6 +353,10 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { // self.currentError = nil self.updateFontState(self.currentFontState, force: true) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index daaad7f05c..c3288ad5a1 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -80,6 +80,8 @@ private final class BrowserScreenComponent: CombinedComponent { let performAction = context.component.performAction let performHoldAction = context.component.performHoldAction + let isTablet = environment.metrics.isTablet + let navigationContent: AnyComponentWithIdentity? var navigationLeftItems: [AnyComponentWithIdentity] var navigationRightItems: [AnyComponentWithIdentity] @@ -106,6 +108,7 @@ private final class BrowserScreenComponent: CombinedComponent { AddressBarContentComponent( theme: environment.theme, strings: environment.strings, + metrics: environment.metrics, url: context.component.contentState?.url ?? "", isSecure: context.component.contentState?.isSecure ?? false, isExpanded: context.component.presentationState.addressFocused, @@ -126,7 +129,7 @@ private final class BrowserScreenComponent: CombinedComponent { ) } - if context.component.presentationState.addressFocused { + if context.component.presentationState.addressFocused && !isTablet { navigationLeftItems = [] navigationRightItems = [] } else { @@ -146,6 +149,68 @@ private final class BrowserScreenComponent: CombinedComponent { ) ] + if isTablet { + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "minimize", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPictureButton", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ) + + let canGoBack = context.component.contentState?.canGoBack ?? false + let canGoForward = context.component.contentState?.canGoForward ?? false + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "back", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Back", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateBack) + } + ) + ) + ) + ) + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "forward", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Forward", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateForward) + } + ) + ) + ) + ) + } + navigationRightItems = [ AnyComponentWithIdentity( id: "settings", @@ -168,6 +233,65 @@ private final class BrowserScreenComponent: CombinedComponent { ) ) ] + + if isTablet { + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "bookmarks", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ) + ) + ), + at: 0 + ) + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "share", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Chat List/NavigationShare", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.share) + } + ) + ) + ), + at: 0 + ) + navigationRightItems.append( + AnyComponentWithIdentity( + id: "openIn", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ) + ) + ) + ) + } } } @@ -183,12 +307,16 @@ private final class BrowserScreenComponent: CombinedComponent { topInset: environment.statusBarHeight, height: environment.navigationHeight - environment.statusBarHeight, sideInset: environment.safeInsets.left, + metrics: environment.metrics, leftItems: navigationLeftItems, rightItems: navigationRightItems, centerItem: navigationContent, readingProgress: context.component.contentState?.readingProgress ?? 0.0, loadingProgress: context.component.contentState?.estimatedProgress, - collapseFraction: collapseFraction + collapseFraction: collapseFraction, + activate: { + performAction.invoke(.expand) + } ), availableSize: context.availableSize, transition: context.transition @@ -235,22 +363,36 @@ private final class BrowserScreenComponent: CombinedComponent { toolbarBottomInset = environment.safeInsets.bottom } - let toolbar = toolbar.update( - component: BrowserToolbarComponent( - backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, - separatorColor: environment.theme.rootController.navigationBar.separatorColor, - textColor: environment.theme.rootController.navigationBar.primaryTextColor, - bottomInset: toolbarBottomInset, - sideInset: environment.safeInsets.left, - item: toolbarContent, - collapseFraction: collapseFraction - ), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(toolbar - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) - ) + var toolbarSize: CGFloat = 0.0 + if isTablet && !context.component.presentationState.isSearching { + + } else { + let toolbar = toolbar.update( + component: BrowserToolbarComponent( + backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, + separatorColor: environment.theme.rootController.navigationBar.separatorColor, + textColor: environment.theme.rootController.navigationBar.primaryTextColor, + bottomInset: toolbarBottomInset, + sideInset: environment.safeInsets.left, + item: toolbarContent, + collapseFraction: collapseFraction + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(toolbar + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) + .appear(ComponentTransition.Appear { _, view, transition in + transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: view.frame.height), to: CGPoint(), additive: true) + }) + .disappear(ComponentTransition.Disappear { view, transition, completion in + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 0.0, y: view.frame.height), additive: true, completion: { _ in + completion() + }) + }) + ) + toolbarSize = toolbar.size.height + } if context.component.presentationState.addressFocused { let addressList = addressList.update( @@ -258,15 +400,17 @@ private final class BrowserScreenComponent: CombinedComponent { context: context.component.context, theme: environment.theme, strings: environment.strings, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), navigateTo: { url in performAction.invoke(.navigateTo(url)) } ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbar.size.height), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize), transition: context.transition ) context.add(addressList .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) + .clipsToBounds(true) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) @@ -314,6 +458,7 @@ public class BrowserScreen: ViewController, MinimizableController { case openAddressBar case closeAddressBar case navigateTo(String) + case expand } fileprivate final class Node: ViewControllerTracingNode { @@ -568,6 +713,10 @@ public class BrowserScreen: ViewController, MinimizableController { updatedState.addressFocused = false return updatedState }) + case .expand: + if let content = self.content.last { + content.resetScrolling() + } } } @@ -626,6 +775,14 @@ public class BrowserScreen: ViewController, MinimizableController { } self.pushContent(content, transition: .spring(duration: 0.4)) } + browserContent.openAppUrl = { [weak self] url in + guard let self else { + return + } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: self.presentationData, navigationController: self.controller?.navigationController as? NavigationController, dismissInput: { [weak self] in + self?.view.window?.endEditing(true) + }) + } browserContent.present = { [weak self] c, a in guard let self, let controller = self.controller else { return @@ -989,6 +1146,10 @@ public class BrowserScreen: ViewController, MinimizableController { } } + if update.isReset { + scrollingPanelOffsetFraction = 0.0 + } + if scrollingPanelOffsetFraction != self.scrollingPanelOffsetFraction { self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction self.requestLayout(transition: transition) @@ -1168,7 +1329,7 @@ public class BrowserScreen: ViewController, MinimizableController { private let context: AccountContext private let subject: Subject - var openPreviousOnClose = false + private var openPreviousOnClose = false private var validLayout: ContainerViewLayout? @@ -1184,9 +1345,10 @@ public class BrowserScreen: ViewController, MinimizableController { // "application/vnd.openxmlformats-officedocument.presentationml.presentation" ] - public init(context: AccountContext, subject: Subject) { + public init(context: AccountContext, subject: Subject, openPreviousOnClose: Bool = false) { self.context = context self.subject = subject + self.openPreviousOnClose = openPreviousOnClose super.init(navigationBarPresentationData: nil) @@ -1218,13 +1380,35 @@ public class BrowserScreen: ViewController, MinimizableController { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) + var navigationHeight = self.navigationLayout(layout: layout).navigationFrame.height + if layout.metrics.isTablet, layout.size.width > layout.size.height { + navigationHeight += 6.0 + } + self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationHeight, transition: ComponentTransition(transition)) } public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + self.openPreviousOnClose = false self.node.minimize(topEdgeOffset: topEdgeOffset, damping: 180.0, initialVelocity: initialVelocity) } + private var didPlayAppearanceAnimation = false + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !self.didPlayAppearanceAnimation, let layout = self.validLayout, layout.metrics.isTablet { + self.node.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.openPreviousOnClose, let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer, let controller = minimizedContainer.controllers.last { + navigationController.maximizeViewController(controller, animated: true) + } + } + public var isMinimized = false { didSet { if let webContent = self.node.content.last as? BrowserWebContent { @@ -1339,8 +1523,8 @@ private final class BrowserContentComponent: Component { let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right) - - component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, transition: transition) + + component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, safeInsets: component.insets, transition: transition) transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) return availableSize diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index ba252fbbf7..67b9bfa514 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -18,6 +18,8 @@ import UndoUI import LottieComponent import MultilineTextComponent import UrlEscaping +import UrlHandling +import SaveProgressScreen private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -115,11 +117,23 @@ private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { } } +final class WebView: WKWebView { + var customBottomInset: CGFloat = 0.0 { + didSet { + self.setNeedsLayout() + } + } + + override var safeAreaInsets: UIEdgeInsets { + return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) + } +} + final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - let webView: WKWebView + let webView: WebView private let errorView: ComponentHostView private var currentError: Error? @@ -139,6 +153,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private let faviconDisposable = MetaDisposable() var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -155,23 +170,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let configuration = WKWebViewConfiguration() -// let bundle = Bundle.main -// let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? "" -// var proxyServerHost = "magic.org" if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String { proxyServerHost = hostValue } configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite") configuration.allowsInlineMediaPlayback = true -// configuration.applicationNameForUserAgent = "Telegram-iOS/\(bundleVersion)" if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { configuration.mediaTypesRequiringUserActionForPlayback = [] } else { configuration.mediaPlaybackRequiresUserAction = false } - self.webView = WKWebView(frame: CGRect(), configuration: configuration) + self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true if #available(iOS 11.0, *) { @@ -201,6 +212,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU super.init(frame: .zero) + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self self.webView.scrollView.clipsToBounds = false @@ -214,7 +228,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil) if #available(iOS 15.0, *) { - self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } if #available(iOS 16.4, *) { @@ -244,8 +257,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } - if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) } } @@ -433,13 +446,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { - self.validLayout = (size, insets, fullInsets) + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets, safeInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) - let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - fullInsets.bottom)) + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top)) var refresh = false if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { refresh = true @@ -450,6 +463,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.reloadInputViews() } + self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: fullInsets.bottom, right: 0.0) + self.webView.customBottomInset = max(insets.bottom, safeInsets.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) @@ -460,11 +476,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU ErrorComponent( theme: self.presentationData.theme, title: self.presentationData.strings.Browser_ErrorTitle, - text: error.localizedDescription + text: error.localizedDescription, + insets: insets ) ), environment: {}, - containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) + containerSize: CGSize(width: size.width, height: size.height) ) if self.errorView.superview == nil { self.addSubview(self.errorView) @@ -521,14 +538,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + guard !self.ignoreUpdatesUntilScrollingStopped else { + return + } let scrollView = self.webView.scrollView let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { @@ -558,6 +586,63 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + private var ignoreUpdatesUntilScrollingStopped = false + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + if self.webView.scrollView.isDecelerating { + self.ignoreUpdatesUntilScrollingStopped = true + } + } + + @available(iOS 13.0, *) + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { + self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in + if download { + decisionHandler(.download, preferences) + } else { + decisionHandler(.cancel, preferences) + } + }) + } else { + if let url = navigationAction.request.url?.absoluteString { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + decisionHandler(.cancel, preferences) + self.minimize() + self.openAppUrl(url) + } else { + decisionHandler(.allow, preferences) + } + } else { + decisionHandler(.allow, preferences) + } + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if navigationResponse.canShowMIMEType { + decisionHandler(.allow) + } else if #available(iOS 14.5, *) { + decisionHandler(.download) + } else { + decisionHandler(.cancel) + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url?.absoluteString { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + decisionHandler(.cancel) + self.minimize() + self.openAppUrl(url) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { self.currentError = nil self.updateFontState(self.currentFontState, force: true) @@ -578,8 +663,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { self.currentError = nil } - if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) } } @@ -596,7 +681,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.close() } - @available(iOSApplicationExtension 15.0, iOS 15.0, *) + @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { decisionHandler(.prompt) } @@ -699,12 +784,37 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU completionHandler(configuration) } + private func presentDownloadConfirmation(fileName: String, proceed: @escaping (Bool) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.WebBrowser_Download_Confirmation(fileName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + proceed(false) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Download_Download, action: { + if !completed { + completed = true + proceed(true) + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + proceed(false) + } + } + } + self.present(alertController, nil) + } + private func open(url: String, new: Bool) { let subject: BrowserScreen.Subject = .webPage(url: url) if new, let navigationController = self.getNavigationController() { navigationController._keepModalDismissProgress = true self.minimize() - let controller = BrowserScreen(context: self.context, subject: subject) + let controller = BrowserScreen(context: self.context, subject: subject, openPreviousOnClose: true) navigationController._keepModalDismissProgress = true navigationController.pushViewController(controller) } else { @@ -881,15 +991,18 @@ private final class ErrorComponent: CombinedComponent { let theme: PresentationTheme let title: String let text: String + let insets: UIEdgeInsets init( theme: PresentationTheme, title: String, - text: String + text: String, + insets: UIEdgeInsets ) { self.theme = theme self.title = title self.text = text + self.insets = insets } static func ==(lhs: ErrorComponent, rhs: ErrorComponent) -> Bool { @@ -902,10 +1015,14 @@ private final class ErrorComponent: CombinedComponent { if lhs.text != rhs.text { return false } + if lhs.insets != rhs.insets { + return false + } return true } static var body: Body { + let background = Child(Rectangle.self) let animation = Child(LottieComponent.self) let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) @@ -916,6 +1033,17 @@ private final class ErrorComponent: CombinedComponent { let animationSpacing: CGFloat = 8.0 let textSpacing: CGFloat = 8.0 + let constrainedWidth = context.availableSize.width - 76.0 - context.component.insets.left - context.component.insets.right + + let background = background.update( + component: Rectangle(color: context.component.theme.list.plainBackgroundColor), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + let animation = animation.update( component: LottieComponent( content: LottieComponent.AppBundleContent(name: "ChatListNoResults") @@ -924,9 +1052,6 @@ private final class ErrorComponent: CombinedComponent { availableSize: CGSize(width: animationSize, height: animationSize), transition: .immediate ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + animation.size.height / 2.0)) - ) contentHeight += animation.size.height + animationSpacing let title = title.update( @@ -939,12 +1064,9 @@ private final class ErrorComponent: CombinedComponent { horizontalAlignment: .center ), environment: {}, - availableSize: context.availableSize, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), transition: .immediate ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0)) - ) contentHeight += title.size.height + textSpacing let text = text.update( @@ -958,15 +1080,27 @@ private final class ErrorComponent: CombinedComponent { maximumNumberOfLines: 0 ), environment: {}, - availableSize: context.availableSize, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), transition: .immediate ) - context.add(text - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + text.size.height / 2.0)) - ) contentHeight += text.size.height - - return CGSize(width: context.availableSize.width, height: contentHeight) + + var originY = floor((context.availableSize.height - contentHeight) / 2.0) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + animation.size.height / 2.0)) + ) + originY += animation.size.height + animationSpacing + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + title.size.height / 2.0)) + ) + originY += title.size.height + textSpacing + + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + text.size.height / 2.0)) + ) + + return context.availableSize } } } diff --git a/submodules/BrowserUI/Sources/Punycode.swift b/submodules/BrowserUI/Sources/Punycode.swift new file mode 100644 index 0000000000..2edbbb22e3 --- /dev/null +++ b/submodules/BrowserUI/Sources/Punycode.swift @@ -0,0 +1,317 @@ +// +// Created by kojirof on 2018-11-19. +// Copyright (c) 2018 Gumob. All rights reserved. +// + +//MIT License +// +//Copyright (c) 2018 Gumob +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +import Foundation + +public class Punycode { + + /// Punycode RFC 3492 + /// See https://www.ietf.org/rfc/rfc3492.txt for standard details + + private let base: Int = 36 + private let tMin: Int = 1 + private let tMax: Int = 26 + private let skew: Int = 38 + private let damp: Int = 700 + private let initialBias: Int = 72 + private let initialN: Int = 128 + + /// RFC 3492 specific + private let delimiter: Character = "-" + private let lowercase: ClosedRange = "a"..."z" + private let digits: ClosedRange = "0"..."9" + private let lettersBase: UInt32 = Character("a").unicodeScalars.first!.value + private let digitsBase: UInt32 = Character("0").unicodeScalars.first!.value + + /// IDNA + private let ace: String = "xn--" + + private func adaptBias(_ delta: Int, _ numberOfPoints: Int, _ firstTime: Bool) -> Int { + var delta: Int = delta + if firstTime { + delta /= damp + } else { + delta /= 2 + } + delta += delta / numberOfPoints + var k: Int = 0 + while delta > ((base - tMin) * tMax) / 2 { + delta /= base - tMin + k += base + } + return k + ((base - tMin + 1) * delta) / (delta + skew) + } + + /// Maps a punycode character to index + private func punycodeIndex(for character: Character) -> Int? { + if lowercase.contains(character) { + return Int(character.unicodeScalars.first!.value - lettersBase) + } else if digits.contains(character) { + return Int(character.unicodeScalars.first!.value - digitsBase) + 26 /// count of lowercase letters range + } else { + return nil + } + } + + /// Maps an index to corresponding punycode character + private func punycodeValue(for digit: Int) -> Character? { + guard digit < base else { return nil } + if digit < 26 { + return Character(UnicodeScalar(lettersBase.advanced(by: digit))!) + } else { + return Character(UnicodeScalar(digitsBase.advanced(by: digit - 26))!) + } + } + + /// Decodes punycode encoded string to original representation + /// + /// - Parameter punycode: Punycode encoding (RFC 3492) + /// - Returns: Decoded string or nil if the input cannot be decoded + public func decodePunycode(_ punycode: Substring) -> String? { + var n: Int = initialN + var i: Int = 0 + var bias: Int = initialBias + var output: [Character] = [] + var inputPosition = punycode.startIndex + + let delimiterPosition: Substring.Index = punycode.lastIndex(of: delimiter) ?? punycode.startIndex + if delimiterPosition > punycode.startIndex { + output.append(contentsOf: punycode[..= bias + tMax ? tMax : k - bias) + if digit < t { + break + } + w *= base - t + k += base + } while !punycodeInput.isEmpty + bias = adaptBias(i - oldI, output.count + 1, oldI == 0) + n += i / (output.count + 1) + i %= (output.count + 1) + guard n >= 0x80, let scalar = UnicodeScalar(n) else { + return nil + } + output.insert(Character(scalar), at: i) + i += 1 + } + + return String(output) + } + + /// Encodes string to punycode (RFC 3492) + /// + /// - Parameter input: Input string + /// - Returns: Punycode encoded string + public func encodePunycode(_ input: Substring) -> String? { + var n: Int = initialN + var delta: Int = 0 + var bias: Int = initialBias + var output: String = "" + for scalar in input.unicodeScalars { + if scalar.isASCII { + let char = Character(scalar) + output.append(char) + } else if !scalar.isValid { + return nil /// Encountered a scalar out of acceptable range + } + } + var handled: Int = output.count + let basic: Int = handled + if basic > 0 { + output.append(delimiter) + } + while handled < input.unicodeScalars.count { + var minimumCodepoint: Int = 0x10FFFF + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < minimumCodepoint && scalar.value >= n { + minimumCodepoint = Int(scalar.value) + } + } + delta += (minimumCodepoint - n) * (handled + 1) + n = minimumCodepoint + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < n { + delta += 1 + } else if scalar.value == n { + var q: Int = delta + var k: Int = base + while true { + let t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias) + if q < t { + break + } + guard let character: Character = punycodeValue(for: t + ((q - t) % (base - t))) else { return nil } + output.append(character) + q = (q - t) / (base - t) + k += base + } + guard let character: Character = punycodeValue(for: q) else { return nil } + output.append(character) + bias = adaptBias(delta, handled + 1, handled == basic) + delta = 0 + handled += 1 + } + } + delta += 1 + n += 1 + } + + return output + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + public func encodeIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.rangeOfCharacter(from: CharacterSet.urlHostAllowed.inverted) != nil { + guard let encoded: String = part.lowercased().punycodeEncoded else { return nil } + output += ace + encoded + } else { + output += part + } + } + return output + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + public func decodedIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.hasPrefix(ace) { + guard let decoded: String = part.dropFirst(ace.count).punycodeDecoded else { return nil } + output += decoded + } else { + output += part + } + } + return output + } +} + +private extension Substring { + func lastIndex(of element: Character) -> String.Index? { + var position: Index = endIndex + while position > startIndex { + position = self.index(before: position) + if self[position] == element { + return position + } + } + return nil + } +} + +private extension UnicodeScalar { + var isValid: Bool { + return value < 0xD880 || (value >= 0xE000 && value <= 0x1FFFFF) + } +} + +public extension Substring { + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return Punycode().encodePunycode(self) + } + + /// Returns new string decoded from punycode representation (RFC 3492) + /// + /// - Returns: Original string or nil if the string doesn't contain correct encoding + var punycodeDecoded: String? { + return Punycode().decodePunycode(self) + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + var idnaEncoded: String? { + return Punycode().encodeIDNA(self) + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + var idnaDecoded: String? { + return Punycode().decodedIDNA(self) + } +} + +public extension String { + + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return self[.. Void)? @@ -20,12 +21,14 @@ final class SectionHeaderComponent: Component { theme: PresentationTheme, style: Style, title: String, + insets: UIEdgeInsets, actionTitle: String?, action: (() -> Void)? ) { self.theme = theme self.style = style self.title = title + self.insets = insets self.actionTitle = actionTitle self.action = action } @@ -40,6 +43,9 @@ final class SectionHeaderComponent: Component { if lhs.title != rhs.title { return false } + if lhs.insets != rhs.insets { + return false + } if lhs.actionTitle != rhs.actionTitle { return false } @@ -73,7 +79,7 @@ final class SectionHeaderComponent: Component { self.state = state let height: CGFloat = 28.0 - let leftInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + component.insets.left let rightInset: CGFloat = 0.0 let previousTitleFrame = self.title.view?.frame diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 4ac67d6a96..127b853313 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -88,8 +88,7 @@ private final class ItemNode: ASDisplayNode { title = presentationData.strings.ChatList_Search_FilterChannels icon = nil case .apps: - //TODO:localize - title = "Apps" + title = presentationData.strings.ChatList_Search_FilterApps icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 57c0091630..bcb62df9a9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -254,16 +254,15 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } } else if case .apps = key { - //TODO:localize if case .popularApps = section { - header = ChatListSearchItemHeader(type: .text("POPULAR APPS", 1), theme: theme, strings: strings) + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionPopularApps, 1), theme: theme, strings: strings) } else { if let isChannelsTabExpanded { - header = ChatListSearchItemHeader(type: .text("APPS YOU USE", 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { toggleChannelsTabExpanded() }) } else { - header = ChatListSearchItemHeader(type: .text("APPS YOU USE", 0), theme: theme, strings: strings, actionTitle: nil, action: nil) + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } } } else { @@ -1454,8 +1453,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if key == .channels { emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_RecommendedChannelsEmpty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) } else if key == .apps { - //TODO:localize - emptyRecentTextNode.attributedText = NSAttributedString(string: "No Apps Found", font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) + emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_Apps_Empty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) } self.emptyRecentTextNode = emptyRecentTextNode @@ -3417,7 +3415,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { continue } let peerNotificationSettings = notificationSettings[id] - //TODO:localize let subpeerSummary: RecentlySearchedPeerSubpeerSummary? = nil var peerStoryStats: PeerStoryStats? if let value = storyStats[peer.id] { diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 58dd462609..9b8015eaf4 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -552,7 +552,8 @@ public final class SparseItemGrid: ASDisplayNode { } var contentBottomOffset: CGFloat { - return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height + let bottomInset = self.layout?.containerLayout.insets.bottom ?? 0.0 + return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height - bottomInset } let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index a294b33a05..13650ccf00 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -2049,6 +2049,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let peer = Promise() peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + let canViewStatsValue = Atomic(value: true) let peerData = context.engine.data.get( TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), @@ -2081,6 +2082,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData + let _ = canViewStatsValue.swap(canViewStats) + var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { isGroup = true @@ -2157,9 +2160,17 @@ public func channelStatsController(context: AccountContext, updatedPresentationD case .stats: index = 0 case .boosts: - index = 1 + if canViewStats { + index = 1 + } else { + index = 0 + } case .monetization: - index = 2 + if canViewStats { + index = 2 + } else { + index = 1 + } } var tabs: [String] = [] if canViewStats { @@ -2195,12 +2206,21 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } controller.titleControlValueChanged = { value in updateState { state in + let canViewStats = canViewStatsValue.with { $0 } let section: ChannelStatsSection switch value { case 0: - section = .stats + if canViewStats { + section = .stats + } else { + section = .boosts + } case 1: - section = .boosts + if canViewStats { + section = .boosts + } else { + section = .monetization + } case 2: section = .monetization let _ = (ApplicationSpecificNotice.monetizationIntroDismissed(accountManager: context.sharedContext.accountManager) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index dc9a008717..b4a2ac7239 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -2627,8 +2627,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController if !isScheduled && canSpeak { if #available(iOS 15.0, *) { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Microphone Modes", textColor: .primary, icon: { theme in + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 7480b59332..7fe98db357 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -520,7 +520,7 @@ public final class MediaEditor { self.renderer.consume(main: .texture(texture, time, hasTransparency), additional: additionalTexture.flatMap { .texture($0, time, false) }, render: true, displayEnabled: false) } - private func setupSource() { + private func setupSource(andPlay: Bool) { guard let renderTarget = self.previewView else { return } @@ -830,6 +830,9 @@ public final class MediaEditor { self.setupTimeObservers() Queue.mainQueue().justDispatch { let startPlayback = { + guard andPlay else { + return + } player.playImmediately(atRate: 1.0) // additionalPlayer?.playImmediately(atRate: 1.0) self.audioPlayer?.playImmediately(atRate: 1.0) @@ -941,13 +944,13 @@ public final class MediaEditor { self.audioDelayTimer = nil } - public func attachPreviewView(_ previewView: MediaEditorPreviewView) { + public func attachPreviewView(_ previewView: MediaEditorPreviewView, andPlay: Bool) { self.previewView?.renderer = nil self.previewView = previewView previewView.renderer = self.renderer - self.setupSource() + self.setupSource(andPlay: andPlay) } private var skipRendering = false @@ -1118,8 +1121,9 @@ public final class MediaEditor { self.initialSeekPosition = position return } - self.renderer.setRate(1.0) - if !play { + if play { + self.renderer.setRate(1.0) + } else { self.player?.pause() self.additionalPlayer?.pause() self.audioPlayer?.pause() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index c5f8979db7..e74d17b390 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -64,6 +64,7 @@ swift_library( "//submodules/WebsiteType", "//submodules/UrlEscaping", "//submodules/DeviceLocationManager", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index db9363e5bb..db1dba00b3 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -225,7 +225,7 @@ private final class MediaCoverScreenComponent: Component { transition: transition, component: AnyComponent(Button( content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Cancel, font: Font.regular(17.0), textColor: .white))) ), action: { [weak controller] in controller?.requestDismiss(animated: true) @@ -546,6 +546,7 @@ final class MediaCoverScreen: ViewController { fileprivate let mediaEditor: Signal fileprivate let previewView: MediaEditorPreviewView fileprivate let portalView: PortalView + fileprivate let exclusive: Bool func withMediaEditor(_ f: @escaping (MediaEditor) -> Void) { let _ = (self.mediaEditor @@ -564,12 +565,14 @@ final class MediaCoverScreen: ViewController { context: AccountContext, mediaEditor: Signal, previewView: MediaEditorPreviewView, - portalView: PortalView + portalView: PortalView, + exclusive: Bool ) { self.context = context self.mediaEditor = mediaEditor self.previewView = previewView self.portalView = portalView + self.exclusive = exclusive super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal @@ -601,7 +604,9 @@ final class MediaCoverScreen: ViewController { self.dismissed() self.node.animateOutToEditor(completion: { - self.dismiss() + if !self.exclusive { + self.dismiss() + } }) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 2bae267beb..41c0d95ca2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -48,6 +48,7 @@ import StickerPackEditTitleController import StickerPickerScreen import UIKitRuntimeUtils import ImageObjectSeparation +import SaveProgressScreen private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -2809,7 +2810,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) if controller.isEditingStoryCover { - self.openCoverSelection(immediate: true) + Queue.mainQueue().justDispatch { + self.openCoverSelection(exclusive: true) + } } } @@ -2984,7 +2987,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 }) } - mediaEditor.attachPreviewView(self.previewView) + mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false)) if case .empty = effectiveSubject { self.stickerMaskDrawingView?.emptyColor = .black @@ -4704,7 +4707,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func openCoverSelection(immediate: Bool) { + func openCoverSelection(exclusive: Bool) { guard let portalView = PortalView(matchPosition: false) else { return } @@ -4721,12 +4724,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate context: self.context, mediaEditor: self.mediaEditorPromise.get(), previewView: self.previewView, - portalView: portalView + portalView: portalView, + exclusive: exclusive ) coverController.dismissed = { [weak self] in if let self { - self.animateInFromTool() - self.requestCompletion(playHaptic: false) + if exclusive { + self.controller?.requestDismiss(saveDraft: false, animated: true) + } else { + self.animateInFromTool() + self.requestCompletion(playHaptic: false) + } } } coverController.completed = { [weak self] position, image in @@ -4737,7 +4745,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(coverController, in: .current) self.coverScreen = coverController - if immediate { + if exclusive { self.isDisplayingTool = .cover self.requestUpdate(transition: .immediate) } else { @@ -5226,7 +5234,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) self.animateOutToTool(tool: .tools) case .cover: - self.openCoverSelection(immediate: false) + self.openCoverSelection(exclusive: false) } } }, @@ -5923,7 +5931,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editCoverImpl = { [weak self, weak controller] in if let self { - self.node.openCoverSelection(immediate: false) + self.node.openCoverSelection(exclusive: false) } if let controller { controller.dismiss() diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 08ecaa53f2..579bd7b8d9 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -246,8 +246,7 @@ final class MiniAppListScreenComponent: Component { ) -> CGFloat { let rightButtons: [AnyComponentWithIdentity] = [] - //TODO:localize - let titleText: String = "Examples" + let titleText: String = strings.MiniAppList_Title let closeTitle: String = strings.Common_Close let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( @@ -316,12 +315,11 @@ final class MiniAppListScreenComponent: Component { containerSize: size ) - //TODO:localize let sectionHeaderSize = self.sectionHeader.update( transition: transition, component: AnyComponent(ListHeaderComponent( theme: theme, - title: "APPS THAT ACCEPT STARS" + title: strings.MiniAppList_ListSectionHeader )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 91606b4938..dd48610be3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -1126,8 +1126,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat case .storyArchive: title = presentationData.strings.PeerInfo_PaneArchivedStories case .botPreview: - //TODO:localize - title = "Preview" + title = presentationData.strings.PeerInfo_PaneBotPreviews case .media: title = presentationData.strings.PeerInfo_PaneMedia case .files: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c63e6a276f..2ae84f8593 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1357,12 +1357,20 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else if hasAbout || hasWebApp { var actionButton: PeerInfoScreenLabeledValueItem.Button? if hasWebApp { - //TODO:localize - actionButton = PeerInfoScreenLabeledValueItem.Button(title: "Open App", action: { + actionButton = PeerInfoScreenLabeledValueItem.Button(title: presentationData.strings.PeerInfo_OpenAppButton, action: { guard let parentController = interaction.getController() else { return } + if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == user.id && mainController.source == .generic { + navigationController.maximizeViewController(controller, animated: true) + return + } + } + } + context.sharedContext.openWebApp( context: context, parentController: parentController, @@ -1390,8 +1398,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - //TODO:localize - items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy).", linkAction: { action in + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooterAdmin, linkAction: { action in if case let .tap(url) = action { context.sharedContext.applicationBindings.openUrl(url) } @@ -1399,8 +1406,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese currentPeerInfoSection = .peerInfoTrailing } else if actionButton != nil { - //TODO:localize - items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy).", linkAction: { action in + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooter, linkAction: { action in if case let .tap(url) = action { context.sharedContext.applicationBindings.openUrl(url) } @@ -10894,14 +10900,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var items: [ContextMenuItem] = [] let strings = self.presentationData.strings - let _ = strings - - //TODO:localize var ignoreNextActions = false if pane.canAddMoreBotPreviews() { - items.append(.action(ContextMenuActionItem(text: "Add Preview", icon: { theme in + items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in if ignoreNextActions { @@ -10947,8 +10950,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) if let language = pane.currentBotPreviewLanguage { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Delete \(language.name)", textColor: .destructive, icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak pane] _, a in if ignoreNextActions { @@ -14029,192 +14031,3 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } - -/*private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - - let botName: String - let botAddress: String - let botVerified: Bool - if case let .inline(bot) = source { - botName = bot.compactDisplayTitle - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified - } else { - botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - botVerified = peer.isVerified - } - - let _ = botAddress - - let openWebView = { [weak parentController] in - guard let parentController else { - return - } - - if source == .menu { - if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { - for controller in minimizedContainer.controllers { - if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu { - navigationController.maximizeViewController(controller, animated: true) - return - } - } - } - - var fullSize = false - if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - 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) - //TODO:localize - //updatedPresentationData - let controller = standaloneWebAppController(context: context, updatedPresentationData: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in - guard let parentController else { - return - } - let _ = parentController - //ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getInputContainerNode: { - return nil - }, completion: { - }, willDismiss: { - }, didDismiss: { - }, getNavigationController: { [weak parentController] () -> NavigationController? in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - } else if simple { - var isInline = false - var botId = peer.id - var botName = botName - var botAddress = "" - var botVerified = false - if case let .inline(bot) = source { - isInline = true - botId = bot.id - botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified - } - - let _ = botAddress - - let _ = ((context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme))) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in - guard let parentController else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: context, updatedPresentationData: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, requestSwitchInline: { query, chatTypes, completion in - //ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getNavigationController: { [weak parentController] in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - }, error: { [weak parentController] error in - guard let parentController else { - return - } - parentController.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - }) - } else { - let _ = ((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: nil)) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in - guard let parentController else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let context = context - 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 controller = standaloneWebAppController(context: context, updatedPresentationData: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, completion: { - }, getNavigationController: { [weak parentController] in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - }, error: { [weak parentController] error in - guard let parentController else { - return - } - parentController.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - }) - } - } - - var botPeer = peer - if case let .inline(bot) = source { - botPeer = bot - } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in - guard let parentController else { - return - } - if value { - openWebView() - } else { - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: nil, peer: botPeer, completion: { _ in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openWebView() - }, showMore: nil) - parentController.present(controller, in: .window(.root)) - } - }) -}*/ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index 5ba2d19928..b26f1644a1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/SaveToCameraRoll", "//submodules/ShareController", "//submodules/OpenInExternalAppUI", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index e9fccb53a3..dbbf2ff7e3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -14,7 +14,7 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen +import SaveProgressScreen import SaveToCameraRoll final class PeerInfoStoryGridScreenComponent: Component { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 6bdc9c2581..3cf859f663 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -14,7 +14,6 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen import SaveToCameraRoll import ShareController import OpenInExternalAppUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 791dae59df..15327d43df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1629,6 +1629,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? + private var listBottomInset: CGFloat? private let ready = Promise() private var didSetReady: Bool = false @@ -2053,10 +2054,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } - let backgroundColor = presentationData.theme.list.mediaPlaceholderColor - let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) - - return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + if case .botPreview = scope { + let backgroundColor = presentationData.theme.list.plainBackgroundColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } else { + let backgroundColor = presentationData.theme.list.mediaPlaceholderColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } } self.itemGridBinding.updateShimmerLayersImpl = { [weak self] layer in @@ -2496,8 +2504,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) if isPinned && self.canReorder() { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return @@ -2561,8 +2568,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if canManage, case .botPreview = self.scope, self.canReorder() { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return @@ -2730,11 +2736,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let title: String if state.totalCount == 0 { if case .botPreview = self.scope { - //TODO:localize if state.isLoading { - title = "loading" + title = self.presentationData.strings.BotPreviews_SubtitleLoading } else { - title = "no preview added" + title = self.presentationData.strings.BotPreviews_SubtitleEmpty } } else { title = "" @@ -2748,12 +2753,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } } else if case .botPreview = self.scope { - //TODO:localize - if state.totalCount == 1 { - title = "1 preview" - } else { - title = "\(state.totalCount) previews" - } + title = self.presentationData.strings.BotPreviews_SubtitleCount(Int32(state.totalCount)) } else { title = "" } @@ -3349,18 +3349,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - //TODO:localize - let title: String - if mappedMedia.count == 1 { - title = "Delete 1 Preview?" - } else { - title = "Delete \(mappedMedia.count) Previews?" - } + let title: String = presentationData.strings.BotPreviews_SheetDeleteTitle(Int32(mappedMedia.count)) controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self] in + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self] in dismissAction() guard let self else { @@ -3396,7 +3390,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if case .botPreview = self.scope, self.canManageStories { self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) - self.updateBotPreviewFooter(size: currentParams.size, bottomInset: currentParams.bottomInset, transition: transition) + self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition) } } } @@ -3564,11 +3558,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.botPreviewLanguageTab = botPreviewLanguageTab } - //TODO:localize var languageItems: [TabSelectorComponent.Item] = [] languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_main"), - title: "Main" + title: self.presentationData.strings.BotPreviews_LanguageTab_Main )) for language in self.currentBotPreviewLanguages { languageItems.append(TabSelectorComponent.Item( @@ -3578,7 +3571,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_add"), - title: "+ Add Language" + title: self.presentationData.strings.BotPreviews_LanguageTab_Add )) var selectedLanguageId = "_main" if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { @@ -3645,9 +3638,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let text: String if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { isMainLanguage = false - text = "This preview will be displayed for all users who have \(language.name) set as their language." + + text = self.presentationData.strings.BotPreviews_TranslationFooter_Text(language.name).string } else { - text = "This preview will be shown by default. You can also add translations into specific languages." + text = self.presentationData.strings.BotPreviews_DefaultFooter_Text } let botPreviewFooterSize = botPreviewFooter.update( @@ -3659,14 +3653,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr animationName: nil, title: nil, text: text, - actionTitle: "Add Preview", + actionTitle: self.presentationData.strings.BotPreviews_Empty_Add, action: { [weak self] in guard let self else { return } - self.emptyAction?() + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } }, - additionalActionTitle: isMainLanguage ? "Create a Translation" : nil, + additionalActionTitle: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_AddTranslation : nil, additionalAction: { [weak self] in guard let self else { return @@ -3675,12 +3673,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentAddBotPreviewLanguage() } }, - additionalActionSeparator: isMainLanguage ? "or" : nil + additionalActionSeparator: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) ) - let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset - botPreviewFooterSize.height - bottomInset), size: botPreviewFooterSize) + let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset + 16.0), size: botPreviewFooterSize) if let botPreviewFooterView = botPreviewFooter.view { if botPreviewFooterView.superview == nil { self.view.addSubview(botPreviewFooterView) @@ -3746,16 +3744,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset))) } - let defaultBottomInset = bottomInset + var listBottomInset = bottomInset var bottomInset = bottomInset if case .botPreview = self.scope, self.canManageStories { updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) gridTopInset += 50.0 - updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) if let botPreviewFooterView = self.botPreviewFooter?.view { - bottomInset += 18.0 + botPreviewFooterView.bounds.height + listBottomInset += 18.0 + botPreviewFooterView.bounds.height } } @@ -3886,6 +3884,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height } else if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case .botPreview = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) @@ -3932,6 +3931,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil if let selectionPanelView = selectionPanel.view { @@ -4018,7 +4018,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr emptyStateView = ComponentView() self.emptyStateView = emptyStateView } - //TODO:localize var isMainLanguage = true if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { @@ -4032,16 +4031,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: nil, - title: "No Preview", - text: "Upload up to \(self.maxBotPreviewCount) screenshots and video demos for your mini app.", - actionTitle: self.canManageStories ? "Add Preview" : nil, + title: presentationData.strings.BotPreviews_Empty_Title, + text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)), + actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil, action: { [weak self] in guard let self else { return } - self.emptyAction?() + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } }, - additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil, + additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil, additionalAction: { if isMainLanguage { self.presentAddBotPreviewLanguage() @@ -4049,7 +4052,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentDeleteBotPreviewLanguage() } }, - additionalActionSeparator: self.canManageStories ? "or" : nil + additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) @@ -4133,11 +4136,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: listBottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } + self.listBottomInset = listBottomInset if case .botPreview = self.scope, self.canManageStories { - updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) } } @@ -4238,7 +4242,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentAddBotPreviewLanguage() { - self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in + let excludeIds: [String] = self.currentBotPreviewLanguages.map(\.id) + self.parentController?.push(LanguageSelectionScreen(context: self.context, excludeIds: excludeIds, selectLocalization: { [weak self] info in guard let self else { return } @@ -4246,12 +4251,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr })) } + public func presentUnableToAddMorePreviewsAlert() { + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + }) + ], parseMarkdown: true), in: .window(.root)) + } + public func presentDeleteBotPreviewLanguage() { - //TODO:localize - self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [ + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { }), - TextAlertAction(type: .destructiveAction, title: "OK", action: { [weak self] in + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { return } diff --git a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD new file mode 100644 index 0000000000..e9de562525 --- /dev/null +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SaveProgressScreen", + module_name = "SaveProgressScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/LottieAnimationComponent", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift similarity index 100% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift rename to submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift index 04229ce599..9d698608da 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -11,6 +11,7 @@ import SearchUI public class LanguageSelectionScreen: ViewController { private let context: AccountContext + private let excludeIds: [String] private let selectLocalization: (LocalizationInfo) -> Void private var controllerNode: LanguageSelectionScreenNode { @@ -29,8 +30,9 @@ public class LanguageSelectionScreen: ViewController { private var previousContentOffset: ListViewVisibleContentOffset? - public init(context: AccountContext, selectLocalization: @escaping (LocalizationInfo) -> Void) { + public init(context: AccountContext, excludeIds: [String] = [], selectLocalization: @escaping (LocalizationInfo) -> Void) { self.context = context + self.excludeIds = excludeIds self.selectLocalization = selectLocalization self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -40,8 +42,7 @@ public class LanguageSelectionScreen: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modal - //TODO:localize - self.title = "Add a Translation" + self.title = self.presentationData.strings.BotPreviews_SelectLanguage_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -92,7 +93,7 @@ public class LanguageSelectionScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in + self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, excludeIds: self.excludeIds, requestActivateSearch: { [weak self] in self?.activateSearch() }, requestDeactivateSearch: { [weak self] in self?.deactivateSearch() diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift index ef9438ed6e..b31bb91475 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -294,6 +294,7 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private weak var navigationBar: NavigationBar? + private let excludeIds: [String] private let requestActivateSearch: () -> Void private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void @@ -316,11 +317,12 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { private var currentListState: LocalizationListState? - init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, excludeIds: [String], requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { self.context = context self.presentationData = presentationData self.presentationDataValue.set(.single(presentationData)) self.navigationBar = navigationBar + self.excludeIds = excludeIds self.requestActivateSearch = requestActivateSearch self.requestDeactivateSearch = requestDeactivateSearch self.present = present @@ -362,6 +364,12 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { var entries: [LanguageListEntry] = [] var existingIds = Set() + var localizationListState = localizationListState + localizationListState.availableOfficialLocalizations = localizationListState.availableOfficialLocalizations.filter { + !strongSelf.excludeIds.contains($0.languageCode) + } + localizationListState.availableSavedLocalizations = [] + if !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 48e2f5ea58..7f516bdd6d 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -976,6 +976,7 @@ open class SpaceWarpView4: UIView, SpaceWarpView { meshView.frame = CGRect(origin: CGPoint(), size: size) let pixelStep = CGPoint() + //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 7e63c31eea..4d244dd6fc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -98,6 +98,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen", "//submodules/TelegramUI/Components/SliderContextItem", "//submodules/TelegramUI/Components/InteractiveTextComponent", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a1f5b25732..4c70ed44a4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -43,6 +43,7 @@ import TelegramUIPreferences import StoryFooterPanelComponent import TelegramNotices import SliderContextItem +import SaveProgressScreen public final class StoryAvailableReactions: Equatable { let reactionItems: [ReactionItem] @@ -5655,8 +5656,7 @@ public final class StoryItemSetContainerComponent: Component { let deleteTitle: String if case let .user(user) = component.slice.peer, user.botInfo != nil { - //TODO:localize - deleteTitle = "Delete Preview" + deleteTitle = component.strings.BotPreview_ViewContextDelete } else { deleteTitle = component.strings.Story_ContextDeleteStory } @@ -6604,8 +6604,7 @@ public final class StoryItemSetContainerComponent: Component { if case let .user(user) = component.slice.peer, let botInfo = user.botInfo { if botInfo.flags.contains(.canEdit) { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.BotPreviews_MenuReorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -6616,8 +6615,8 @@ public final class StoryItemSetContainerComponent: Component { component.reorder() }))) - items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.destructiveColor) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, a in a(.default) diff --git a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js index fcb419a5bd..34e5ddd288 100644 --- a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js +++ b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js @@ -9,6 +9,10 @@ var uiWebview_SearchResultCount = 0; keyword - string to search */ +function isElementVisible(e) { + return true +} + function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { if (element) { if (element.nodeType == 3) { // Text node @@ -86,7 +90,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { } else if (element.nodeType == 1) { // Element node - if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') { + if (element.nodeName.toLowerCase() != 'select' && isElementVisible(element)) { for (var i=element.childNodes.length-1; i>=0; i--) { uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword); } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index c25368c74b..4897df2846 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1008,11 +1008,12 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + let urlScheme = (parsedUrl.scheme ?? "").lowercased() var isInternetUrl = false - if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + if ["http", "https"].contains(urlScheme) { isInternetUrl = true } - if parsedUrl.scheme == "tonsite" { + if urlScheme == "tonsite" { isInternetUrl = true } @@ -1032,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur settings = .defaultSettings } if accessChallengeData.data.isLockable { - if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil { + if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 59b63dd909..d9398de8a4 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -437,7 +437,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.visibility = true } - //TODO:localize self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) displayUndo = false diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f8fb3f07c5..af85edc262 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -1203,7 +1203,11 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String var url = url if !url.contains("://") && !url.hasPrefix("tel:") && !url.hasPrefix("mailto:") && !url.hasPrefix("calshow:") { if !(url.hasPrefix("http") || url.hasPrefix("https")) { - url = "http://\(url)" + if let mappedURL = URL(string: "https://\(url)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + url = "tonsite://\(url)" + } else { + url = "http://\(url)" + } } }