diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7259ebd7e73..f504b663f71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,8 @@ name: CI on: - push: - branches: [ master ] + # push: + # branches: [ master ] workflow_dispatch: diff --git a/.gitignore b/.gitignore index a0d74e1226d..880ac900786 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ xcodeproj.bazelrc build-input/* **/*.pyc *.pyc +submodules/**/.build/* +swiftgram-scripts +Swiftgram/Playground/custom_bazel_path.bzl \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 7b06b63b024..d0bdd1b7a67 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,6 @@ - [submodule "submodules/rlottie/rlottie"] path = submodules/rlottie/rlottie - url=../rlottie.git + url=https://github.com/TelegramMessenger/rlottie.git [submodule "build-system/bazel-rules/rules_apple"] path = build-system/bazel-rules/rules_apple url=https://github.com/ali-fareed/rules_apple.git @@ -16,7 +15,7 @@ url=https://github.com/bazelbuild/rules_swift.git url = https://github.com/telegramdesktop/libtgvoip.git [submodule "submodules/TgVoipWebrtc/tgcalls"] path = submodules/TgVoipWebrtc/tgcalls -url=../tgcalls.git +url=https://github.com/TelegramMessenger/tgcalls.git [submodule "third-party/libvpx/libvpx"] path = third-party/libvpx/libvpx url = https://github.com/webmproject/libvpx.git diff --git a/README.md b/README.md index 79f325aa139..1f754271a88 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# Swiftgram + +Supercharged Telegram fork for iOS + +[](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8) + +- Download: [App Store](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8) +- Telegram channel: https://t.me/swiftgram +- Telegram chat: https://t.me/swiftgramchat +- TestFlight beta, local chats, translations and other [@SwiftgramLinks](https://t.me/s/SwiftgramLinks) + +Swiftgram's compilation steps are the same as for the official app. Below you'll find a complete compilation guide based on the official app. + # Telegram iOS Source Code Compilation Guide We welcome all developers to use our API and source code to create applications on our platform. @@ -16,7 +29,7 @@ There are several things we require from **all developers** for the moment. ## Get the Code ``` -git clone --recursive -j8 https://github.com/TelegramMessenger/Telegram-iOS.git +git clone --recursive -j8 https://github.com/Swiftgram/Telegram-iOS.git ``` ## Setup Xcode @@ -29,7 +42,7 @@ Install Xcode (directly from https://developer.apple.com/download/applications o ``` openssl rand -hex 8 ``` -2. Create a new Xcode project. Use `Telegram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier. +2. Create a new Xcode project. Use `Swiftgram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier. 3. Open `Keychain Access` and navigate to `Certificates`. Locate `Apple Development: your@email.address (XXXXXXXXXX)` and double tap the certificate. Under `Details`, locate `Organizational Unit`. This is the Team ID. 4. Edit `build-system/template_minimal_development_configuration.json`. Use data from the previous steps. diff --git a/Swiftgram/AppleStyleFolders/BUILD b/Swiftgram/AppleStyleFolders/BUILD new file mode 100644 index 00000000000..0924cf28e86 --- /dev/null +++ b/Swiftgram/AppleStyleFolders/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "AppleStyleFolders", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/AppleStyleFolders/Sources/File.swift b/Swiftgram/AppleStyleFolders/Sources/File.swift new file mode 100644 index 00000000000..ec7a3027174 --- /dev/null +++ b/Swiftgram/AppleStyleFolders/Sources/File.swift @@ -0,0 +1,1034 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import SGSimpleSettings + +private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { + private let pressed: () -> Void + + private let contentImageNode: ASImageNode + + private var theme: PresentationTheme? + + init(pressed: @escaping () -> Void) { + self.pressed = pressed + + self.contentImageNode = ASImageNode() + + super.init() + + self.addSubnode(self.contentImageNode) + + self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside) + } + + @objc private func pressedEvent() { + self.pressed() + } + + func update(theme: PresentationTheme) -> CGSize { + let size = CGSize(width: 18.0, height: 18.0) + if self.theme !== theme { + self.theme = theme + self.contentImageNode.image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(rgb: 0xbbbbbb).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(rgb: 0xffffff).cgColor) + context.setLineWidth(1.5) + context.setLineCap(.round) + context.move(to: CGPoint(x: 6.38, y: 6.38)) + context.addLine(to: CGPoint(x: 11.63, y: 11.63)) + context.strokePath() + context.move(to: CGPoint(x: 6.38, y: 11.63)) + context.addLine(to: CGPoint(x: 11.63, y: 6.38)) + context.strokePath() + }) + } + + self.contentImageNode.frame = CGRect(origin: CGPoint(), size: size) + + return size + } +} + +private final class ItemNode: ASDisplayNode { + private let pressed: () -> Void + private let requestedDeletion: () -> Void + + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let extractedBackgroundNode: ASImageNode + private let titleNode: ImmediateTextNode + private let shortTitleNode: ImmediateTextNode + private let badgeContainerNode: ASDisplayNode + private let badgeTextNode: ImmediateTextNode + private let badgeBackgroundActiveNode: ASImageNode + private let badgeBackgroundInactiveNode: ASImageNode + + private var deleteButtonNode: ItemNodeDeleteButtonNode? + private let buttonNode: HighlightTrackingButtonNode + + private var isSelected: Bool = false + private(set) var unreadCount: Int = 0 + + private var isReordering: Bool = false + + private var theme: PresentationTheme? + + init(pressed: @escaping () -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) { + self.pressed = pressed + self.requestedDeletion = requestedDeletion + + self.extractedContainerNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.extractedBackgroundNode = ASImageNode() + self.extractedBackgroundNode.alpha = 0.0 + + let titleInset: CGFloat = 4.0 + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + + self.shortTitleNode = ImmediateTextNode() + self.shortTitleNode.displaysAsynchronously = false + self.shortTitleNode.alpha = 0.0 + self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + + self.badgeContainerNode = ASDisplayNode() + + self.badgeTextNode = ImmediateTextNode() + self.badgeTextNode.displaysAsynchronously = false + + self.badgeBackgroundActiveNode = ASImageNode() + self.badgeBackgroundActiveNode.displaysAsynchronously = false + self.badgeBackgroundActiveNode.displayWithoutProcessing = true + + self.badgeBackgroundInactiveNode = ASImageNode() + self.badgeBackgroundInactiveNode.displaysAsynchronously = false + self.badgeBackgroundInactiveNode.displayWithoutProcessing = true + self.badgeBackgroundInactiveNode.isHidden = true + + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.extractedContainerNode.contentNode.addSubnode(self.extractedBackgroundNode) + self.extractedContainerNode.contentNode.addSubnode(self.titleNode) + self.extractedContainerNode.contentNode.addSubnode(self.shortTitleNode) + self.badgeContainerNode.addSubnode(self.badgeBackgroundActiveNode) + self.badgeContainerNode.addSubnode(self.badgeBackgroundInactiveNode) + self.badgeContainerNode.addSubnode(self.badgeTextNode) + self.extractedContainerNode.contentNode.addSubnode(self.badgeContainerNode) + self.extractedContainerNode.contentNode.addSubnode(self.buttonNode) + + self.containerNode.addSubnode(self.extractedContainerNode) + self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode + self.addSubnode(self.containerNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + contextGesture(strongSelf.extractedContainerNode, gesture) + } + + self.extractedContainerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 32.0, color: strongSelf.isSelected ? UIColor(rgb: 0xbbbbbb) : UIColor(rgb: 0xf1f1f1)) + } + transition.updateAlpha(node: strongSelf.extractedBackgroundNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundNode.image = nil + } + }) + } + } + + @objc private func buttonPressed() { + self.pressed() + } + + func updateText(title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, isSelected: Bool, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + + self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor) + self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor) + } + + self.containerNode.isGestureEnabled = !isEditing && !isReordering + self.buttonNode.isUserInteractionEnabled = !isEditing && !isReordering + + self.isSelected = isSelected + self.unreadCount = unreadCount + + transition.updateAlpha(node: self.containerNode, alpha: isReordering && isAllChats ? 0.5 : 1.0) + + if isReordering && !isAllChats { + if self.deleteButtonNode == nil { + let deleteButtonNode = ItemNodeDeleteButtonNode(pressed: { [weak self] in + self?.requestedDeletion() + }) + self.extractedContainerNode.contentNode.addSubnode(deleteButtonNode) + self.deleteButtonNode = deleteButtonNode + if case .animated = transition { + deleteButtonNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25) + deleteButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + } else if let deleteButtonNode = self.deleteButtonNode { + self.deleteButtonNode = nil + transition.updateTransformScale(node: deleteButtonNode, scale: 0.1) + transition.updateAlpha(node: deleteButtonNode, alpha: 0.0, completion: { [weak deleteButtonNode] _ in + deleteButtonNode?.removeFromSupernode() + }) + } + + transition.updateAlpha(node: self.badgeContainerNode, alpha: (isReordering || unreadCount == 0) ? 0.0 : 1.0) + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: isSelected ? presentationData.theme.contextMenu.badgeForegroundColor : presentationData.theme.list.itemSecondaryTextColor) + self.shortTitleNode.attributedText = NSAttributedString(string: shortTitle, font: Font.bold(17.0), textColor: isSelected ? presentationData.theme.contextMenu.badgeForegroundColor : presentationData.theme.list.itemSecondaryTextColor) + if unreadCount != 0 { + self.badgeTextNode.attributedText = NSAttributedString(string: "\(unreadCount)", font: Font.regular(14.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor) + self.badgeBackgroundActiveNode.isHidden = !isSelected && !unreadHasUnmuted + self.badgeBackgroundInactiveNode.isHidden = isSelected || unreadHasUnmuted + } + + if self.isReordering != isReordering { + self.isReordering = isReordering + if self.isReordering && !isAllChats { + self.startShaking() + } else { + self.layer.removeAnimation(forKey: "shaking_position") + self.layer.removeAnimation(forKey: "shaking_rotation") + } + } + } + + func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) { + let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + + let shortTitleSize = self.shortTitleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) + self.shortTitleNode.frame = CGRect(origin: CGPoint(x: -self.shortTitleNode.insets.left, y: floor((height - shortTitleSize.height) / 2.0)), size: shortTitleSize) + + if let deleteButtonNode = self.deleteButtonNode { + if let theme = self.theme { + let deleteButtonSize = deleteButtonNode.update(theme: theme) + deleteButtonNode.frame = CGRect(origin: CGPoint(x: -deleteButtonSize.width + 7.0, y: 5.0), size: deleteButtonSize) + } + } + + let badgeSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let badgeInset: CGFloat = 4.0 + let badgeBackgroundFrame = CGRect(origin: CGPoint(x: titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + 5.0, y: floor((height - 18.0) / 2.0)), size: CGSize(width: max(18.0, badgeSize.width + badgeInset * 2.0), height: 18.0)) + self.badgeContainerNode.frame = badgeBackgroundFrame + self.badgeBackgroundActiveNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size) + self.badgeBackgroundInactiveNode.frame = CGRect(origin: CGPoint(), size: badgeBackgroundFrame.size) + self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize) + + let width: CGFloat + if self.unreadCount == 0 || self.isReordering { + if !self.isReordering { + self.badgeContainerNode.alpha = 0.0 + } + width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + } else { + if !self.isReordering { + self.badgeContainerNode.alpha = 1.0 + } + width = badgeBackgroundFrame.maxX + } + + return (width, shortTitleSize.width - self.shortTitleNode.insets.left - self.shortTitleNode.insets.right) + } + + func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.titleNode, alpha: useShortTitle ? 0.0 : 1.0) + transition.updateAlpha(node: self.shortTitleNode, alpha: useShortTitle ? 1.0 : 0.0) + + self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height)) + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(x: self.extractedBackgroundNode.frame.minX, y: 0.0), size: CGSize(width: self.extractedBackgroundNode.frame.width, height: size.height)) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset) + self.extractedContainerNode.hitTestSlop = self.hitTestSlop + self.extractedContainerNode.contentNode.hitTestSlop = self.hitTestSlop + self.containerNode.hitTestSlop = self.hitTestSlop + + let extractedBackgroundHeight: CGFloat = 32.0 + let extractedBackgroundInset: CGFloat = 14.0 + self.extractedBackgroundNode.frame = CGRect(origin: CGPoint(x: -extractedBackgroundInset, y: floor((size.height - extractedBackgroundHeight) / 2.0)), size: CGSize(width: size.width + extractedBackgroundInset * 2.0, height: extractedBackgroundHeight)) + } + + func animateBadgeIn() { + if !self.isReordering { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.badgeContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) + transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) + } + } + + func animateBadgeOut() { + if !self.isReordering { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.badgeContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 1.0) + transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1) + } + } + + private func startShaking() { + func degreesToRadians(_ x: CGFloat) -> CGFloat { + return .pi * x / 180.0 + } + + let duration: Double = 0.4 + let displacement: CGFloat = 1.0 + let degreesRotation: CGFloat = 2.0 + + let negativeDisplacement = -1.0 * displacement + let position = CAKeyframeAnimation.init(keyPath: "position") + position.beginTime = 0.8 + position.duration = duration + position.values = [ + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: 0, y: 0)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)), + NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)), + NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)) + ] + position.calculationMode = .linear + position.isRemovedOnCompletion = false + position.repeatCount = Float.greatestFiniteMagnitude + position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + position.isAdditive = true + + let transform = CAKeyframeAnimation.init(keyPath: "transform") + transform.beginTime = 2.6 + transform.duration = 0.3 + transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ) + transform.values = [ + degreesToRadians(-1.0 * degreesRotation), + degreesToRadians(degreesRotation), + degreesToRadians(-1.0 * degreesRotation) + ] + transform.calculationMode = .linear + transform.isRemovedOnCompletion = false + transform.repeatCount = Float.greatestFiniteMagnitude + transform.isAdditive = true + transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100)) + + self.layer.add(position, forKey: "shaking_position") + self.layer.add(transform, forKey: "shaking_rotation") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let deleteButtonNode = self.deleteButtonNode { + if deleteButtonNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + return deleteButtonNode.view + } + } + return super.hitTest(point, with: event) + } +} + +private final class ItemNodePair { + let regular: ItemNode + let highlighted: ItemNode + + init(regular: ItemNode, highlighted: ItemNode) { + self.regular = regular + self.highlighted = highlighted + } +} + +public final class AppleStyleFoldersNode: ASDisplayNode { + private let scrollNode: ASScrollNode + private let itemsBackgroundView: UIVisualEffectView + private let itemsBackgroundTintNode: ASImageNode + + private let selectedBackgroundNode: ASImageNode + private var itemNodePairs: [ChatListFilterTabEntryId: ItemNodePair] = [:] + private var itemsContainer: ASDisplayNode + private var highlightedItemsClippingContainer: ASDisplayNode + private var highlightedItemsContainer: ASDisplayNode + + var tabSelected: ((ChatListFilterTabEntryId, Bool) -> Void)? + var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? + var addFilter: (() -> Void)? + var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? + + private var reorderingGesture: ReorderingGestureRecognizer? + private var reorderingItem: ChatListFilterTabEntryId? + private var reorderingItemPosition: (initial: CGFloat, offset: CGFloat)? + private var reorderingAutoScrollAnimator: ConstantDisplayLinkAnimator? + private var reorderedItemIds: [ChatListFilterTabEntryId]? + private lazy var hapticFeedback = { HapticFeedback() }() + + private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)? + + var reorderedFilterIds: [Int32]? { + return self.reorderedItemIds.flatMap { + $0.compactMap { + switch $0 { + case .all: + return 0 + case let .filter(id): + return id + } + } + } + } + + override init() { + self.scrollNode = ASScrollNode() + + self.itemsBackgroundView = UIVisualEffectView() + self.itemsBackgroundView.clipsToBounds = true + self.itemsBackgroundView.layer.cornerRadius = 20.0 + + self.itemsBackgroundTintNode = ASImageNode() + self.itemsBackgroundTintNode.displaysAsynchronously = false + self.itemsBackgroundTintNode.displayWithoutProcessing = true + + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.displayWithoutProcessing = true + + self.itemsContainer = ASDisplayNode() + + self.highlightedItemsClippingContainer = ASDisplayNode() + self.highlightedItemsClippingContainer.clipsToBounds = true + self.highlightedItemsClippingContainer.layer.cornerRadius = 16.0 + + self.highlightedItemsContainer = ASDisplayNode() + + super.init() + + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.canCancelContentTouches = true + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.addSubnode(self.scrollNode) + self.scrollNode.view.addSubview(self.itemsBackgroundView) + self.scrollNode.addSubnode(self.itemsBackgroundTintNode) + self.scrollNode.addSubnode(self.itemsContainer) + self.scrollNode.addSubnode(self.selectedBackgroundNode) + self.scrollNode.addSubnode(self.highlightedItemsClippingContainer) + self.highlightedItemsClippingContainer.addSubnode(self.highlightedItemsContainer) + + let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in + guard let strongSelf = self else { + return false + } + for (id, itemNodePair) in strongSelf.itemNodePairs { + if itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view).contains(point) { + if case .all = id { + return false + } + return true + } + } + return false + }, began: { [weak self] point in + guard let strongSelf = self, let _ = strongSelf.currentParams else { + return + } + for (id, itemNodePair) in strongSelf.itemNodePairs { + let itemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view) + if itemFrame.contains(point) { + strongSelf.hapticFeedback.impact() + + strongSelf.reorderingItem = id + itemNodePair.regular.frame = itemFrame + strongSelf.reorderingAutoScrollAnimator = ConstantDisplayLinkAnimator(update: { + guard let strongSelf = self, let currentLocation = strongSelf.reorderingGesture?.currentLocation else { + return + } + let edgeWidth: CGFloat = 20.0 + if currentLocation.x <= edgeWidth { + var contentOffset = strongSelf.scrollNode.view.contentOffset + contentOffset.x = max(0.0, contentOffset.x - 3.0) + strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false) + } else if currentLocation.x >= strongSelf.bounds.width - edgeWidth { + var contentOffset = strongSelf.scrollNode.view.contentOffset + contentOffset.x = max(0.0, min(strongSelf.scrollNode.view.contentSize.width - strongSelf.scrollNode.bounds.width, contentOffset.x + 3.0)) + strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false) + } + }) + strongSelf.reorderingAutoScrollAnimator?.isPaused = false + strongSelf.addSubnode(itemNodePair.regular) + + strongSelf.reorderingItemPosition = (itemNodePair.regular.frame.minX, 0.0) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + return + } + } + }, ended: { [weak self] in + guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else { + return + } + if let itemNodePair = strongSelf.itemNodePairs[reorderingItem] { + let projectedItemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.scrollNode.view) + itemNodePair.regular.frame = projectedItemFrame + strongSelf.itemsContainer.addSubnode(itemNodePair.regular) + } + + strongSelf.reorderingItem = nil + strongSelf.reorderingItemPosition = nil + strongSelf.reorderingAutoScrollAnimator?.invalidate() + strongSelf.reorderingAutoScrollAnimator = nil + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + }, moved: { [weak self] offset in + guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else { + return + } + if let reorderingItemNodePair = strongSelf.itemNodePairs[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) { + for (id, itemNodePair) in strongSelf.itemNodePairs { + guard let itemIndex = reorderedItemIds.firstIndex(of: id) else { + continue + } + if id != reorderingItem { + let itemFrame = itemNodePair.regular.view.convert(itemNodePair.regular.bounds, to: strongSelf.view) + if reorderingItemNodePair.regular.frame.intersects(itemFrame) { + let targetIndex: Int + if reorderingItemNodePair.regular.frame.midX < itemFrame.midX { + targetIndex = max(1, itemIndex - 1) + } else { + targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex)) + } + if targetIndex != currentItemIndex { + strongSelf.hapticFeedback.tap() + + var updatedReorderedItemIds = reorderedItemIds + if targetIndex > currentItemIndex { + updatedReorderedItemIds.insert(reorderingItem, at: targetIndex + 1) + updatedReorderedItemIds.remove(at: currentItemIndex) + } else { + updatedReorderedItemIds.remove(at: currentItemIndex) + updatedReorderedItemIds.insert(reorderingItem, at: targetIndex) + } + strongSelf.reorderedItemIds = updatedReorderedItemIds + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + } + break + } + } + } + + strongSelf.reorderingItemPosition = (initial, offset) + } + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate) + } + }) + self.reorderingGesture = reorderingGesture + self.view.addGestureRecognizer(reorderingGesture) + reorderingGesture.isEnabled = false + } + + private var previousSelectedAbsFrame: CGRect? + private var previousSelectedFrame: CGRect? + + func cancelAnimations() { + self.selectedBackgroundNode.layer.removeAllAnimations() + self.scrollNode.layer.removeAllAnimations() + self.highlightedItemsContainer.layer.removeAllAnimations() + self.highlightedItemsClippingContainer.layer.removeAllAnimations() + } + + func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) { + let isFirstTime = self.currentParams == nil + let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition + + var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter + let previousScrollBounds = self.scrollNode.bounds + let previousContentWidth = self.scrollNode.view.contentSize.width + + if self.currentParams?.presentationData.theme !== presentationData.theme { + if presentationData.theme.rootController.keyboardColor == .dark { + self.itemsBackgroundView.effect = UIBlurEffect(style: .dark) + } else { + self.itemsBackgroundView.effect = UIBlurEffect(style: .light) + } + self.itemsBackgroundTintNode.image = generateStretchableFilledCircleImage(diameter: 40.0, color: presentationData.theme.rootController.tabBar.backgroundColor) + + let selectedFilterColor: UIColor + if presentationData.theme.rootController.keyboardColor == .dark { + selectedFilterColor = presentationData.theme.list.itemAccentColor + } else { + selectedFilterColor = presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor + } + self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 32.0, color: selectedFilterColor) + } + + if isReordering { + if let reorderedItemIds = self.reorderedItemIds { + let currentIds = Set(reorderedItemIds) + if currentIds != Set(filters.map { $0.id }) { + var updatedReorderedItemIds = reorderedItemIds.filter { id in + return filters.contains(where: { $0.id == id }) + } + for filter in filters { + if !currentIds.contains(filter.id) { + updatedReorderedItemIds.append(filter.id) + } + } + self.reorderedItemIds = updatedReorderedItemIds + } + } else { + self.reorderedItemIds = filters.map { $0.id } + } + } else if self.reorderedItemIds != nil { + self.reorderedItemIds = nil + } + + self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData) + + self.reorderingGesture?.isEnabled = isEditing || isReordering + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + enum BadgeAnimation { + case `in` + case out + } + + var badgeAnimations: [ChatListFilterTabEntryId: BadgeAnimation] = [:] + + var reorderedFilters: [ChatListFilterTabEntry] = filters + if let reorderedItemIds = self.reorderedItemIds { + reorderedFilters = reorderedItemIds.compactMap { id -> ChatListFilterTabEntry? in + if let index = filters.firstIndex(where: { $0.id == id }) { + return filters[index] + } else { + return nil + } + } + } + + for filter in reorderedFilters { + let itemNodePair: ItemNodePair + var itemNodeTransition = transition + var wasAdded = false + if let current = self.itemNodePairs[filter.id] { + itemNodePair = current + } else { + itemNodeTransition = .immediate + wasAdded = true + itemNodePair = ItemNodePair(regular: ItemNode(pressed: { [weak self] in + self?.tabSelected?(filter.id, false) + }, requestedDeletion: { [weak self] in + self?.tabRequestedDeletion?(filter.id) + }, contextGesture: { [weak self] sourceNode, gesture in + guard let strongSelf = self else { + return + } + strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = false + strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = true + strongSelf.scrollNode.view.setContentOffset(strongSelf.scrollNode.view.contentOffset, animated: false) + switch filter { + case let .filter(id, _, _): + strongSelf.contextGesture?(id, sourceNode, gesture, false) + default: + strongSelf.contextGesture?(nil, sourceNode, gesture, false) + } + }), highlighted: ItemNode(pressed: { [weak self] in + self?.tabSelected?(filter.id, false) + }, requestedDeletion: { [weak self] in + self?.tabRequestedDeletion?(filter.id) + }, contextGesture: { [weak self] sourceNode, gesture in + guard let strongSelf = self else { + return + } + switch filter { + case let .filter(id, _, _): + strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = false + strongSelf.scrollNode.view.panGestureRecognizer.isEnabled = true + strongSelf.scrollNode.view.setContentOffset(strongSelf.scrollNode.view.contentOffset, animated: false) + strongSelf.contextGesture?(id, sourceNode, gesture, false) + default: + strongSelf.contextGesture?(nil, sourceNode, gesture, false) + } + })) + self.itemNodePairs[filter.id] = itemNodePair + } + let unreadCount: Int + let unreadHasUnmuted: Bool + var isNoFilter: Bool = false + switch filter { + case let .all(count): + unreadCount = count + unreadHasUnmuted = true + isNoFilter = true + case let .filter(_, _, unread): + unreadCount = unread.value + unreadHasUnmuted = unread.hasUnmuted + } + if !wasAdded && (itemNodePair.regular.unreadCount != 0) != (unreadCount != 0) { + badgeAnimations[filter.id] = (unreadCount != 0) ? .in : .out + } + itemNodePair.regular.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, isSelected: false, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: itemNodeTransition) + itemNodePair.highlighted.updateText(title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, isSelected: true, isEditing: false, isAllChats: isNoFilter, isReordering: isEditing || isReordering, presentationData: presentationData, transition: itemNodeTransition) + } + var removeKeys: [ChatListFilterTabEntryId] = [] + for (id, _) in self.itemNodePairs { + if !filters.contains(where: { $0.id == id }) { + removeKeys.append(id) + } + } + for id in removeKeys { + if let itemNodePair = self.itemNodePairs.removeValue(forKey: id) { + let regular = itemNodePair.regular + let highlighted = itemNodePair.highlighted + transition.updateAlpha(node: regular, alpha: 0.0, completion: { [weak regular] _ in + regular?.removeFromSupernode() + }) + transition.updateTransformScale(node: regular, scale: 0.1) + transition.updateAlpha(node: highlighted, alpha: 0.0, completion: { [weak highlighted] _ in + highlighted?.removeFromSupernode() + }) + transition.updateTransformScale(node: highlighted, scale: 0.1) + } + } + + var tabSizes: [(ChatListFilterTabEntryId, CGSize, CGSize, ItemNodePair, Bool)] = [] + var totalRawTabSize: CGFloat = 0.0 + var selectionFrames: [CGRect] = [] + + for filter in reorderedFilters { + guard let itemNodePair = self.itemNodePairs[filter.id] else { + continue + } + let wasAdded = itemNodePair.regular.supernode == nil + var itemNodeTransition = transition + if wasAdded { + itemNodeTransition = .immediate + self.itemsContainer.addSubnode(itemNodePair.regular) + self.highlightedItemsContainer.addSubnode(itemNodePair.highlighted) + } + let (paneNodeWidth, paneNodeShortWidth) = itemNodePair.regular.updateLayout(height: size.height, transition: itemNodeTransition) + let _ = itemNodePair.highlighted.updateLayout(height: size.height, transition: itemNodeTransition) + let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) + let paneNodeShortSize = CGSize(width: paneNodeShortWidth, height: size.height) + tabSizes.append((filter.id, paneNodeSize, paneNodeShortSize, itemNodePair, wasAdded)) + totalRawTabSize += paneNodeSize.width + + if case .animated = transition, let badgeAnimation = badgeAnimations[filter.id] { + switch badgeAnimation { + case .in: + itemNodePair.regular.animateBadgeIn() + itemNodePair.highlighted.animateBadgeIn() + case .out: + itemNodePair.regular.animateBadgeOut() + itemNodePair.highlighted.animateBadgeOut() + } + } + } + // TODO(swiftgram): Support compact layout + let minSpacing: CGFloat = 30.0 + + let resolvedInitialSideInset: CGFloat = 8.0 + 14.0 + 4.0 + sideInset + + var longTitlesWidth: CGFloat = 0.0 + var shortTitlesWidth: CGFloat = 0.0 + for i in 0 ..< tabSizes.count { + let (_, paneNodeSize, paneNodeShortSize, _, _) = tabSizes[i] + longTitlesWidth += paneNodeSize.width + shortTitlesWidth += paneNodeShortSize.width + } + let totalSpacing = CGFloat(tabSizes.count - 1) * minSpacing + let useShortTitles = (longTitlesWidth + totalSpacing + resolvedInitialSideInset * 2.0) > size.width + + var rawContentWidth = useShortTitles ? shortTitlesWidth : longTitlesWidth + rawContentWidth += totalSpacing + + let resolvedSideInset = max(resolvedInitialSideInset, floor((size.width - rawContentWidth) / 2.0)) + + var leftOffset: CGFloat = resolvedSideInset + + let itemsBackgroundLeftX = leftOffset - 14.0 - 4.0 + + for i in 0 ..< tabSizes.count { + let (itemId, paneNodeLongSize, paneNodeShortSize, itemNodePair, wasAdded) = tabSizes[i] + var itemNodeTransition = transition + if wasAdded { + itemNodeTransition = .immediate + } + + let useShortTitle = itemId == .all && sgUseShortAllChatsTitle(useShortTitles) + let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + + if itemId == self.reorderingItem, let (initial, offset) = self.reorderingItemPosition { + itemNodeTransition.updateSublayerTransformScale(node: itemNodePair.regular, scale: 1.2) + itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 0.9) + let offsetFrame = CGRect(origin: CGPoint(x: initial + offset, y: paneFrame.minY), size: paneFrame.size) + itemNodeTransition.updateFrameAdditive(node: itemNodePair.regular, frame: offsetFrame) + selectionFrames.append(offsetFrame) + } else { + itemNodeTransition.updateSublayerTransformScale(node: itemNodePair.regular, scale: 1.0) + itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 1.0) + if wasAdded { + itemNodePair.regular.frame = paneFrame + itemNodePair.regular.alpha = 0.0 + itemNodeTransition.updateAlpha(node: itemNodePair.regular, alpha: 1.0) + } else { + itemNodeTransition.updateFrameAdditive(node: itemNodePair.regular, frame: paneFrame) + } + selectionFrames.append(paneFrame) + } + + if wasAdded { + itemNodePair.highlighted.frame = paneFrame + itemNodePair.highlighted.alpha = 0.0 + itemNodeTransition.updateAlpha(node: itemNodePair.highlighted, alpha: 1.0) + } else { + itemNodeTransition.updateFrameAdditive(node: itemNodePair.highlighted, frame: paneFrame) + } + + itemNodePair.regular.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: itemNodeTransition) + itemNodePair.regular.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0) + + itemNodePair.highlighted.updateArea(size: paneFrame.size, sideInset: minSpacing / 2.0, useShortTitle: useShortTitle, transition: itemNodeTransition) + itemNodePair.highlighted.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing / 2.0, bottom: 0.0, right: -minSpacing / 2.0) + + leftOffset += paneNodeSize.width + minSpacing + } + leftOffset -= minSpacing + let itemsBackgroundRightX = leftOffset + 14.0 + 4.0 + + leftOffset += resolvedSideInset + + let backgroundFrame = CGRect(origin: CGPoint(x: itemsBackgroundLeftX, y: 0.0), size: CGSize(width: itemsBackgroundRightX - itemsBackgroundLeftX, height: size.height)) + transition.updateFrame(view: self.itemsBackgroundView, frame: backgroundFrame) + transition.updateFrame(node: self.itemsBackgroundTintNode, frame: backgroundFrame) + + self.scrollNode.view.contentSize = CGSize(width: itemsBackgroundRightX + 8.0, height: size.height) + + var selectedFrame: CGRect? + if let selectedFilter = selectedFilter, let currentIndex = reorderedFilters.firstIndex(where: { $0.id == selectedFilter }) { + func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { + return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) + } + + if currentIndex != 0 && transitionFraction > 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex - 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else if currentIndex != filters.count - 1 && transitionFraction < 0.0 { + let currentFrame = selectionFrames[currentIndex] + let previousFrame = selectionFrames[currentIndex + 1] + selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) + } else { + selectedFrame = selectionFrames[currentIndex] + } + } + + transition.updateFrame(node: self.itemsContainer, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)) + + if let selectedFrame = selectedFrame { + let wasAdded = self.selectedBackgroundNode.isHidden + self.selectedBackgroundNode.isHidden = false + let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 14.0, y: floor((size.height - 32.0) / 2.0)), size: CGSize(width: selectedFrame.width + 14.0 * 2.0, height: 32.0)) + if wasAdded { + self.selectedBackgroundNode.frame = lineFrame + self.selectedBackgroundNode.alpha = 0.0 + } else { + transition.updateFrame(node: self.selectedBackgroundNode, frame: lineFrame) + } + transition.updateFrame(node: self.highlightedItemsClippingContainer, frame: lineFrame) + transition.updateFrame(node: self.highlightedItemsContainer, frame: CGRect(origin: CGPoint(x: -lineFrame.minX, y: -lineFrame.minY), size: self.scrollNode.view.contentSize)) + transition.updateAlpha(node: self.selectedBackgroundNode, alpha: isReordering ? 0.0 : 1.0) + transition.updateAlpha(node: self.highlightedItemsClippingContainer, alpha: isReordering ? 0.0 : 1.0) + + if let previousSelectedFrame = self.previousSelectedFrame { + let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) + if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 { + focusOnSelectedFilter = true + } + } + + if focusOnSelectedFilter && self.reorderingItem == nil { + let updatedBounds: CGRect + if transitionFraction.isZero && selectedFilter == reorderedFilters.first?.id { + updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + } else if transitionFraction.isZero && selectedFilter == reorderedFilters.last?.id { + updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size) + } else { + let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) + updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size) + } + self.scrollNode.bounds = updatedBounds + } + transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + + self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) + self.previousSelectedFrame = selectedFrame + } else { + self.selectedBackgroundNode.isHidden = true + self.previousSelectedAbsFrame = nil + self.previousSelectedFrame = nil + } + } +} + +private class ReorderingGestureRecognizerTimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + + super.init() + } + + @objc func timerEvent() { + self.f() + } +} + +private final class ReorderingGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private let shouldBegin: (CGPoint) -> Bool + private let began: (CGPoint) -> Void + private let ended: () -> Void + private let moved: (CGFloat) -> Void + + private var initialLocation: CGPoint? + private var delayTimer: Foundation.Timer? + + var currentLocation: CGPoint? + + init(shouldBegin: @escaping (CGPoint) -> Bool, began: @escaping (CGPoint) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) { + self.shouldBegin = shouldBegin + self.began = began + self.ended = ended + self.moved = moved + + super.init(target: nil, action: nil) + + self.delegate = self + } + + override func reset() { + super.reset() + + self.initialLocation = nil + self.delayTimer?.invalidate() + self.delayTimer = nil + self.currentLocation = nil + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPanGestureRecognizer { + return true + } else { + return false + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + guard let location = touches.first?.location(in: self.view) else { + self.state = .failed + return + } + + if self.state == .possible { + if self.delayTimer == nil { + if !self.shouldBegin(location) { + self.state = .failed + return + } + self.initialLocation = location + let timer = Foundation.Timer(timeInterval: 0.2, target: ReorderingGestureRecognizerTimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.delayTimer = nil + strongSelf.state = .began + strongSelf.began(location) + }, selector: #selector(ReorderingGestureRecognizerTimerTarget.timerEvent), userInfo: nil, repeats: false) + self.delayTimer = timer + RunLoop.main.add(timer, forMode: .common) + } else { + self.state = .failed + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.delayTimer?.invalidate() + + if self.state == .began || self.state == .changed { + self.ended() + } + + self.state = .failed + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + if self.state == .began || self.state == .changed { + self.delayTimer?.invalidate() + self.ended() + self.state = .failed + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + guard let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) else { + return + } + let offset = location.x - initialLocation.x + self.currentLocation = location + + if self.delayTimer != nil { + if abs(offset) > 4.0 { + self.delayTimer?.invalidate() + self.state = .failed + return + } + } else { + if self.state == .began || self.state == .changed { + self.state = .changed + self.moved(offset) + } + } + } +} diff --git a/Swiftgram/ChatControllerImplExtension/BUILD b/Swiftgram/ChatControllerImplExtension/BUILD new file mode 100644 index 00000000000..15c650e14a6 --- /dev/null +++ b/Swiftgram/ChatControllerImplExtension/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "ChatControllerImplExtension", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift b/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift new file mode 100644 index 00000000000..23d5c46b4c2 --- /dev/null +++ b/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift @@ -0,0 +1,225 @@ +import SGSimpleSettings +import Foundation +import UIKit +import Postbox +import SwiftSignalKit +import Display +import AsyncDisplayKit +import TelegramCore +import SafariServices +import MobileCoreServices +import Intents +import LegacyComponents +import TelegramPresentationData +import TelegramUIPreferences +import DeviceAccess +import TextFormat +import TelegramBaseController +import AccountContext +import TelegramStringFormatting +import OverlayStatusController +import DeviceLocationManager +import ShareController +import UrlEscaping +import ContextUI +import ComposePollUI +import AlertUI +import PresentationDataUtils +import UndoUI +import TelegramCallsUI +import TelegramNotices +import GameUI +import ScreenCaptureDetection +import GalleryUI +import OpenInExternalAppUI +import LegacyUI +import InstantPageUI +import LocationUI +import BotPaymentsUI +import DeleteChatPeerActionSheetItem +import HashtagSearchUI +import LegacyMediaPickerUI +import Emoji +import PeerAvatarGalleryUI +import PeerInfoUI +import RaiseToListen +import UrlHandling +import AvatarNode +import AppBundle +import LocalizedPeerData +import PhoneNumberFormat +import SettingsUI +import UrlWhitelist +import TelegramIntents +import TooltipUI +import StatisticsUI +import MediaResources +import GalleryData +import ChatInterfaceState +import InviteLinksUI +import Markdown +import TelegramPermissionsUI +import Speak +import TranslateUI +import UniversalMediaPlayer +import WallpaperBackgroundNode +import ChatListUI +import CalendarMessageScreen +import ReactionSelectionNode +import ReactionListContextMenuContent +import AttachmentUI +import AttachmentTextInputPanelNode +import MediaPickerUI +import ChatPresentationInterfaceState +import Pasteboard +import ChatSendMessageActionUI +import ChatTextLinkEditUI +import WebUI +import PremiumUI +import ImageTransparency +import StickerPackPreviewUI +import TextNodeWithEntities +import EntityKeyboard +import ChatTitleView +import EmojiStatusComponent +import ChatTimerScreen +import MediaPasteboardUI +import ChatListHeaderComponent +import ChatControllerInteraction +import FeaturedStickersScreen +import ChatEntityKeyboardInputNode +import StorageUsageScreen +import AvatarEditorScreen +import ChatScheduleTimeController +import ICloudResources +import StoryContainerScreen +import MoreHeaderButton +import VolumeButtons +import ChatAvatarNavigationNode +import ChatContextQuery +import PeerReportScreen +import PeerSelectionController +import SaveToCameraRoll +import ChatMessageDateAndStatusNode +import ReplyAccessoryPanelNode +import TextSelectionNode +import ChatMessagePollBubbleContentNode +import ChatMessageItem +import ChatMessageItemImpl +import ChatMessageItemView +import ChatMessageItemCommon +import ChatMessageAnimatedStickerItemNode +import ChatMessageBubbleItemNode +import ChatNavigationButton +import WebsiteType +import ChatQrCodeScreen +import PeerInfoScreen +import MediaEditorScreen +import WallpaperGalleryScreen +import WallpaperGridScreen +import VideoMessageCameraScreen +import TopMessageReactions +import AudioWaveform +import PeerNameColorScreen +import ChatEmptyNode +import ChatMediaInputStickerGridItem +import AdsInfoScreen + +extension ChatControllerImpl { + + func forwardMessagesToCloud(messageIds: [MessageId], removeNames: Bool, openCloud: Bool, resetCurrent: Bool = false) { + let _ = (self.context.engine.data.get(EngineDataMap( + messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) + )) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + guard let strongSelf = self else { + return + } + + if resetCurrent { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withoutSelectionState() }) }) + } + + let sortedMessages = messages.values.compactMap { $0?._asMessage() }.sorted { lhs, rhs in + return lhs.id < rhs.id + } + + var attributes: [MessageAttribute] = [] + if removeNames { + attributes.append(ForwardOptionsMessageAttribute(hideNames: true, hideCaptions: false)) + } + + if !openCloud { + Queue.mainQueue().after(0.88) { + strongSelf.chatDisplayNode.hapticFeedback.success() + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + if case .info = value, let strongSelf = self { + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let strongSelf = self, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else { + return + } + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + return true + } + return false + }), in: .current) + } + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.context.account.peerId, messages: sortedMessages.map { message -> EnqueueMessage in + return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) + }) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + guard openCloud else { + return + } + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] _ in + guard let strongSelf = strongSelf else { + return + } + strongSelf.chatDisplayNode.hapticFeedback.success() + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] peer in + guard let strongSelf = strongSelf, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else { + return + } + + var navigationSubject: ChatControllerSubject? = nil + for messageId in messageIds { + if let messageId = messageId { + navigationSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) + break + } + } + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: navigationSubject, keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + } )) + } + }) + }) + } +} diff --git a/Swiftgram/FLEX/BUILD b/Swiftgram/FLEX/BUILD new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Swiftgram/FLEX/FLEX.BUILD b/Swiftgram/FLEX/FLEX.BUILD new file mode 100644 index 00000000000..52e69f69169 --- /dev/null +++ b/Swiftgram/FLEX/FLEX.BUILD @@ -0,0 +1,68 @@ +objc_library( + name = "FLEX", + module_name = "FLEX", + srcs = glob( + ["Classes/**/*"], + exclude = [ + "Classes/Info.plist", + "Classes/Utility/APPLE_LICENSE", + "Classes/Network/OSCache/LICENSE.md", + "Classes/Network/PonyDebugger/LICENSE", + "Classes/GlobalStateExplorers/DatabaseBrowser/LICENSE", + "Classes/GlobalStateExplorers/Keychain/SSKeychain_LICENSE", + "Classes/GlobalStateExplorers/SystemLog/LLVM_LICENSE.TXT", + ] + ), + hdrs = glob([ + "Classes/**/*.h" + ]), + includes = [ + "Classes", + "Classes/Core", + "Classes/Core/Controllers", + "Classes/Core/Views", + "Classes/Core/Views/Cells", + "Classes/Core/Views/Carousel", + "Classes/ObjectExplorers", + "Classes/ObjectExplorers/Sections", + "Classes/ObjectExplorers/Sections/Shortcuts", + "Classes/Network", + "Classes/Network/PonyDebugger", + "Classes/Network/OSCache", + "Classes/Toolbar", + "Classes/Manager", + "Classes/Manager/Private", + "Classes/Editing", + "Classes/Editing/ArgumentInputViews", + "Classes/Headers", + "Classes/ExplorerInterface", + "Classes/ExplorerInterface/Tabs", + "Classes/ExplorerInterface/Bookmarks", + "Classes/GlobalStateExplorers", + "Classes/GlobalStateExplorers/Globals", + "Classes/GlobalStateExplorers/Keychain", + "Classes/GlobalStateExplorers/FileBrowser", + "Classes/GlobalStateExplorers/SystemLog", + "Classes/GlobalStateExplorers/DatabaseBrowser", + "Classes/GlobalStateExplorers/RuntimeBrowser", + "Classes/GlobalStateExplorers/RuntimeBrowser/DataSources", + "Classes/ViewHierarchy", + "Classes/ViewHierarchy/SnapshotExplorer", + "Classes/ViewHierarchy/SnapshotExplorer/Scene", + "Classes/ViewHierarchy/TreeExplorer", + "Classes/Utility", + "Classes/Utility/Runtime", + "Classes/Utility/Runtime/Objc", + "Classes/Utility/Runtime/Objc/Reflection", + "Classes/Utility/Categories", + "Classes/Utility/Categories/Private", + "Classes/Utility/Keyboard" + ], + copts = [ + "-Wno-deprecated-declarations", + "-Wno-strict-prototypes", + "-Wno-unsupported-availability-guard", + ], + deps = [], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/Swiftgram/Playground/.swiftformat b/Swiftgram/Playground/.swiftformat new file mode 100644 index 00000000000..842cb77a795 --- /dev/null +++ b/Swiftgram/Playground/.swiftformat @@ -0,0 +1,3 @@ +--maxwidth 100 +--indent 4 +--disable redundantSelf \ No newline at end of file diff --git a/Swiftgram/Playground/BUILD b/Swiftgram/Playground/BUILD new file mode 100644 index 00000000000..fecebb3c58e --- /dev/null +++ b/Swiftgram/Playground/BUILD @@ -0,0 +1,54 @@ +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@rules_xcodeproj//xcodeproj:defs.bzl", + "top_level_target", + "xcodeproj", +) +load( + "//Swiftgram/Playground:custom_bazel_path.bzl", "custom_bazel_path" +) + +objc_library( + name = "PlaygroundMain", + srcs = [ + "Sources/main.m" + ], +) + + +swift_library( + name = "PlaygroundLib", + srcs = glob(["Sources/**/*.swift"]), + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + ], + visibility = ["//visibility:public"], +) + +ios_application( + name = "Playground", + bundle_id = "app.swiftgram.playground", + families = [ + "iphone", + "ipad", + ], + infoplists = ["Resources/Info.plist"], + minimum_os_version = "14.0", + visibility = ["//visibility:public"], + launch_storyboard = "Resources/LaunchScreen.storyboard", + deps = [":PlaygroundMain", ":PlaygroundLib"], +) + +xcodeproj( + bazel_path = custom_bazel_path(), + name = "Playground_xcodeproj", + build_mode = "bazel", + project_name = "Playground", + tags = ["manual"], + top_level_targets = [ + ":Playground", + ], +) \ No newline at end of file diff --git a/Swiftgram/Playground/README.md b/Swiftgram/Playground/README.md new file mode 100644 index 00000000000..59cee5b3362 --- /dev/null +++ b/Swiftgram/Playground/README.md @@ -0,0 +1,31 @@ +# Swiftgram Playground + +Small app to quickly iterate on components testing without building an entire messenger. + +## Generate Xcode project + +### From root + +```shell +./Swiftgram/Playground/generate_project.py +``` + +### From current directory + +```shell +./generate_project.py +``` + +## Run generated project on simulator + +### From root + +```shell +./Swiftgram/Playground/launch_on_simulator.py +``` + +### From current directory + +```shell +./launch_on_simulator.py +``` diff --git a/Swiftgram/Playground/Resources/Info.plist b/Swiftgram/Playground/Resources/Info.plist new file mode 100644 index 00000000000..95fdf06b7de --- /dev/null +++ b/Swiftgram/Playground/Resources/Info.plist @@ -0,0 +1,39 @@ + + + + + UILaunchScreen + + UILaunchScreen + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + \ No newline at end of file diff --git a/Swiftgram/Playground/Resources/LaunchScreen.storyboard b/Swiftgram/Playground/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000000..865e9329f37 --- /dev/null +++ b/Swiftgram/Playground/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swiftgram/Playground/Sources/AppDelegate.swift b/Swiftgram/Playground/Sources/AppDelegate.swift new file mode 100644 index 00000000000..68beb2d5bd7 --- /dev/null +++ b/Swiftgram/Playground/Sources/AppDelegate.swift @@ -0,0 +1,39 @@ +import UIKit +import SwiftUI +import AsyncDisplayKit +import Display + + +@objc(AppDelegate) +final class AppDelegate: NSObject, UIApplicationDelegate { + var window: UIWindow? + + private var mainWindow: Window1? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + let statusBarHost = ApplicationStatusBarHost() + let (window, hostView) = nativeWindowHostView() + let mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost) + self.mainWindow = mainWindow + hostView.containerView.backgroundColor = UIColor.white + self.window = window + + + let navigationController = NavigationController( + mode: .single, + theme: NavigationControllerTheme( + statusBar: .black, + navigationBar: THEME.navigationBar, + emptyAreaColor: .white + ) + ) + + mainWindow.viewController = navigationController + + navigationController.setViewControllers([mySwiftUIViewController(0)], animated: false) + + self.window?.makeKeyAndVisible() + + return true + } +} diff --git a/Swiftgram/Playground/Sources/AppNavigationSetup.swift b/Swiftgram/Playground/Sources/AppNavigationSetup.swift new file mode 100644 index 00000000000..28b7549d450 --- /dev/null +++ b/Swiftgram/Playground/Sources/AppNavigationSetup.swift @@ -0,0 +1,100 @@ +import UIKit +import SwiftUI +import AsyncDisplayKit +import Display + +public func isKeyboardWindow(window: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: window)) + if #available(iOS 9.0, *) { + if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") { + return true + } + } else { + if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") { + return true + } + } + return false +} + +public func isKeyboardView(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") { + return true + } + return false +} + +public func isKeyboardViewContainer(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") { + return true + } + return false +} + +public class ApplicationStatusBarHost: StatusBarHost { + private let application = UIApplication.shared + + public var isApplicationInForeground: Bool { + switch self.application.applicationState { + case .background: + return false + default: + return true + } + } + + public var statusBarFrame: CGRect { + return self.application.statusBarFrame + } + public var statusBarStyle: UIStatusBarStyle { + get { + return self.application.statusBarStyle + } set(value) { + self.setStatusBarStyle(value, animated: false) + } + } + + public func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) { + if self.shouldChangeStatusBarStyle?(style) ?? true { + self.application.internalSetStatusBarStyle(style, animated: animated) + } + } + + public var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)? + + public func setStatusBarHidden(_ value: Bool, animated: Bool) { + self.application.internalSetStatusBarHidden(value, animation: animated ? .fade : .none) + } + + public var keyboardWindow: UIWindow? { + if #available(iOS 16.0, *) { + return UIApplication.shared.internalGetKeyboard() + } + + for window in UIApplication.shared.windows { + if isKeyboardWindow(window: window) { + return window + } + } + return nil + } + + public var keyboardView: UIView? { + guard let keyboardWindow = self.keyboardWindow else { + return nil + } + + for view in keyboardWindow.subviews { + if isKeyboardViewContainer(view: view) { + for subview in view.subviews { + if isKeyboardView(view: subview) { + return subview + } + } + } + } + return nil + } +} diff --git a/Swiftgram/Playground/Sources/Application.swift b/Swiftgram/Playground/Sources/Application.swift new file mode 100644 index 00000000000..12e8255877d --- /dev/null +++ b/Swiftgram/Playground/Sources/Application.swift @@ -0,0 +1,5 @@ +import UIKit + +@objc(Application) class Application: UIApplication { + +} \ No newline at end of file diff --git a/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift b/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift new file mode 100644 index 00000000000..982fcbf4798 --- /dev/null +++ b/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift @@ -0,0 +1,95 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private final class PlaygroundSplashScreenNode: ASDisplayNode { + private let headerBackgroundNode: ASDisplayNode + private let headerCornerNode: ASImageNode + + private var isDismissed = false + + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + override init() { + self.headerBackgroundNode = ASDisplayNode() + self.headerBackgroundNode.backgroundColor = .black + + self.headerCornerNode = ASImageNode() + self.headerCornerNode.displaysAsynchronously = false + self.headerCornerNode.displayWithoutProcessing = true + self.headerCornerNode.image = generateImage(CGSize(width: 20.0, height: 10.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 20.0, height: 20.0))) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1) + + super.init() + + self.backgroundColor = THEME.list.itemBlocksBackgroundColor + + self.addSubnode(self.headerBackgroundNode) + self.addSubnode(self.headerCornerNode) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + if self.isDismissed { + return + } + self.validLayout = (layout, navigationHeight) + + let headerHeight = navigationHeight + 260.0 + + transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: 0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight))) + transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: 10.0))) + } + + func animateOut(completion: @escaping () -> Void) { + guard let (layout, navigationHeight) = self.validLayout else { + completion() + return + } + self.isDismissed = true + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + let headerHeight = navigationHeight + 260.0 + + transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: -headerHeight - 10.0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight)), completion: { _ in + completion() + }) + transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: CGSize(width: layout.size.width, height: 10.0))) + } +} + +public final class PlaygroundSplashScreen: ViewController { + + public init() { + + let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: true, separatorColor: .clear, badgeBackgroundColor: THEME.navigationBar.badgeBackgroundColor, badgeStrokeColor: THEME.navigationBar.badgeStrokeColor, badgeTextColor: THEME.navigationBar.badgeTextColor) + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: "", close: ""))) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PlaygroundSplashScreenNode() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! PlaygroundSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + public func animateOut(completion: @escaping () -> Void) { + self.statusBar.statusBarStyle = .Black + (self.displayNode as! PlaygroundSplashScreenNode).animateOut(completion: completion) + } +} diff --git a/Swiftgram/Playground/Sources/PlaygroundTheme.swift b/Swiftgram/Playground/Sources/PlaygroundTheme.swift new file mode 100644 index 00000000000..b05d7933461 --- /dev/null +++ b/Swiftgram/Playground/Sources/PlaygroundTheme.swift @@ -0,0 +1,362 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit + + +public final class PlaygroundInfoTheme { + public let buttonBackgroundColor: UIColor + public let buttonTextColor: UIColor + public let incomingFundsTitleColor: UIColor + public let outgoingFundsTitleColor: UIColor + + public init( + buttonBackgroundColor: UIColor, + buttonTextColor: UIColor, + incomingFundsTitleColor: UIColor, + outgoingFundsTitleColor: UIColor + ) { + self.buttonBackgroundColor = buttonBackgroundColor + self.buttonTextColor = buttonTextColor + self.incomingFundsTitleColor = incomingFundsTitleColor + self.outgoingFundsTitleColor = outgoingFundsTitleColor + } +} + +public final class PlaygroundTransactionTheme { + public let descriptionBackgroundColor: UIColor + public let descriptionTextColor: UIColor + + public init( + descriptionBackgroundColor: UIColor, + descriptionTextColor: UIColor + ) { + self.descriptionBackgroundColor = descriptionBackgroundColor + self.descriptionTextColor = descriptionTextColor + } +} + +public final class PlaygroundSetupTheme { + public let buttonFillColor: UIColor + public let buttonForegroundColor: UIColor + public let inputBackgroundColor: UIColor + public let inputPlaceholderColor: UIColor + public let inputTextColor: UIColor + public let inputClearButtonColor: UIColor + + public init( + buttonFillColor: UIColor, + buttonForegroundColor: UIColor, + inputBackgroundColor: UIColor, + inputPlaceholderColor: UIColor, + inputTextColor: UIColor, + inputClearButtonColor: UIColor + ) { + self.buttonFillColor = buttonFillColor + self.buttonForegroundColor = buttonForegroundColor + self.inputBackgroundColor = inputBackgroundColor + self.inputPlaceholderColor = inputPlaceholderColor + self.inputTextColor = inputTextColor + self.inputClearButtonColor = inputClearButtonColor + } +} + +public final class PlaygroundListTheme { + public let itemPrimaryTextColor: UIColor + public let itemSecondaryTextColor: UIColor + public let itemPlaceholderTextColor: UIColor + public let itemDestructiveColor: UIColor + public let itemAccentColor: UIColor + public let itemDisabledTextColor: UIColor + public let plainBackgroundColor: UIColor + public let blocksBackgroundColor: UIColor + public let itemPlainSeparatorColor: UIColor + public let itemBlocksBackgroundColor: UIColor + public let itemBlocksSeparatorColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let sectionHeaderTextColor: UIColor + public let freeTextColor: UIColor + public let freeTextErrorColor: UIColor + public let inputClearButtonColor: UIColor + + public init( + itemPrimaryTextColor: UIColor, + itemSecondaryTextColor: UIColor, + itemPlaceholderTextColor: UIColor, + itemDestructiveColor: UIColor, + itemAccentColor: UIColor, + itemDisabledTextColor: UIColor, + plainBackgroundColor: UIColor, + blocksBackgroundColor: UIColor, + itemPlainSeparatorColor: UIColor, + itemBlocksBackgroundColor: UIColor, + itemBlocksSeparatorColor: UIColor, + itemHighlightedBackgroundColor: UIColor, + sectionHeaderTextColor: UIColor, + freeTextColor: UIColor, + freeTextErrorColor: UIColor, + inputClearButtonColor: UIColor + ) { + self.itemPrimaryTextColor = itemPrimaryTextColor + self.itemSecondaryTextColor = itemSecondaryTextColor + self.itemPlaceholderTextColor = itemPlaceholderTextColor + self.itemDestructiveColor = itemDestructiveColor + self.itemAccentColor = itemAccentColor + self.itemDisabledTextColor = itemDisabledTextColor + self.plainBackgroundColor = plainBackgroundColor + self.blocksBackgroundColor = blocksBackgroundColor + self.itemPlainSeparatorColor = itemPlainSeparatorColor + self.itemBlocksBackgroundColor = itemBlocksBackgroundColor + self.itemBlocksSeparatorColor = itemBlocksSeparatorColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.sectionHeaderTextColor = sectionHeaderTextColor + self.freeTextColor = freeTextColor + self.freeTextErrorColor = freeTextErrorColor + self.inputClearButtonColor = inputClearButtonColor + } +} + +public final class PlaygroundTheme: Equatable { + public let info: PlaygroundInfoTheme + public let transaction: PlaygroundTransactionTheme + public let setup: PlaygroundSetupTheme + public let list: PlaygroundListTheme + public let statusBarStyle: StatusBarStyle + public let navigationBar: NavigationBarTheme + public let keyboardAppearance: UIKeyboardAppearance + public let alert: AlertControllerTheme + public let actionSheet: ActionSheetControllerTheme + + private let resourceCache = PlaygroundThemeResourceCache() + + public init(info: PlaygroundInfoTheme, transaction: PlaygroundTransactionTheme, setup: PlaygroundSetupTheme, list: PlaygroundListTheme, statusBarStyle: StatusBarStyle, navigationBar: NavigationBarTheme, keyboardAppearance: UIKeyboardAppearance, alert: AlertControllerTheme, actionSheet: ActionSheetControllerTheme) { + self.info = info + self.transaction = transaction + self.setup = setup + self.list = list + self.statusBarStyle = statusBarStyle + self.navigationBar = navigationBar + self.keyboardAppearance = keyboardAppearance + self.alert = alert + self.actionSheet = actionSheet + } + + func image(_ key: Int32, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? { + return self.resourceCache.image(key, self, generate) + } + + public static func ==(lhs: PlaygroundTheme, rhs: PlaygroundTheme) -> Bool { + return lhs === rhs + } +} + + +private final class PlaygroundThemeResourceCacheHolder { + var images: [Int32: UIImage] = [:] +} + +private final class PlaygroundThemeResourceCache { + private let imageCache = Atomic(value: PlaygroundThemeResourceCacheHolder()) + + public func image(_ key: Int32, _ theme: PlaygroundTheme, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? { + let result = self.imageCache.with { holder -> UIImage? in + return holder.images[key] + } + if let result = result { + return result + } else { + if let image = generate(theme) { + self.imageCache.with { holder -> Void in + holder.images[key] = image + } + return image + } else { + return nil + } + } + } +} + +enum PlaygroundThemeResourceKey: Int32 { + case itemListCornersBoth + case itemListCornersTop + case itemListCornersBottom + case itemListClearInputIcon + case itemListDisclosureArrow + case navigationShareIcon + case transactionLockIcon + + case clockMin + case clockFrame +} + +func cornersImage(_ theme: PlaygroundTheme, top: Bool, bottom: Bool) -> UIImage? { + if !top && !bottom { + return nil + } + let key: PlaygroundThemeResourceKey + if top && bottom { + key = .itemListCornersBoth + } else if top { + key = .itemListCornersTop + } else { + key = .itemListCornersBottom + } + return theme.image(key.rawValue, { theme in + return generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.setFillColor(theme.list.blocksBackgroundColor.cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + + var corners: UIRectCorner = [] + if top { + corners.insert(.topLeft) + corners.insert(.topRight) + } + if bottom { + corners.insert(.bottomLeft) + corners.insert(.bottomRight) + } + let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) + }) +} + +func itemListClearInputIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.itemListClearInputIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/ClearInput"), color: theme.list.inputClearButtonColor) + }) +} + +func navigationShareIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.navigationShareIcon.rawValue, { theme in + generateTintedImage(image: UIImage(bundleImageName: "Playground/NavigationShare"), color: theme.navigationBar.buttonColor) + }) +} + +func disclosureArrowImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.itemListDisclosureArrow.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/DisclosureArrow"), color: theme.list.itemSecondaryTextColor) + }) +} + +func clockFrameImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.clockFrame.rawValue, { theme in + let color = theme.list.itemSecondaryTextColor + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth)) + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0)) + }) + }) +} + +func clockMinImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.clockMin.rawValue, { theme in + let color = theme.list.itemSecondaryTextColor + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth)) + }) + }) +} + +func PlaygroundTransactionLockIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.transactionLockIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/EncryptedComment"), color: theme.list.itemSecondaryTextColor) + }) +} + + +public let ACCENT_COLOR = UIColor(rgb: 0x007ee5) +public let NAVIGATION_BAR_THEME = NavigationBarTheme( + buttonColor: ACCENT_COLOR, + disabledButtonColor: UIColor(rgb: 0xd0d0d0), + primaryTextColor: .black, + backgroundColor: UIColor(rgb: 0xf7f7f7), + enableBackgroundBlur: true, + separatorColor: UIColor(rgb: 0xb1b1b1), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), + badgeTextColor: .white +) +public let THEME = PlaygroundTheme( + info: PlaygroundInfoTheme( + buttonBackgroundColor: UIColor(rgb: 0x32aafe), + buttonTextColor: .white, + incomingFundsTitleColor: UIColor(rgb: 0x00b12c), + outgoingFundsTitleColor: UIColor(rgb: 0xff3b30) + ), transaction: PlaygroundTransactionTheme( + descriptionBackgroundColor: UIColor(rgb: 0xf1f1f4), + descriptionTextColor: .black + ), setup: PlaygroundSetupTheme( + buttonFillColor: ACCENT_COLOR, + buttonForegroundColor: .white, + inputBackgroundColor: UIColor(rgb: 0xe9e9e9), + inputPlaceholderColor: UIColor(rgb: 0x818086), + inputTextColor: .black, + inputClearButtonColor: UIColor(rgb: 0x7b7b81).withAlphaComponent(0.8) + ), + list: PlaygroundListTheme( + itemPrimaryTextColor: .black, + itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), + itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), + itemDestructiveColor: UIColor(rgb: 0xff3b30), + itemAccentColor: ACCENT_COLOR, + itemDisabledTextColor: UIColor(rgb: 0x8e8e93), + plainBackgroundColor: .white, + blocksBackgroundColor: UIColor(rgb: 0xefeff4), + itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemBlocksBackgroundColor: .white, + itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea), + sectionHeaderTextColor: UIColor(rgb: 0x6d6d72), + freeTextColor: UIColor(rgb: 0x6d6d72), + freeTextErrorColor: UIColor(rgb: 0xcf3030), + inputClearButtonColor: UIColor(rgb: 0xcccccc) + ), + statusBarStyle: .Black, + navigationBar: NAVIGATION_BAR_THEME, + keyboardAppearance: .light, + alert: AlertControllerTheme( + backgroundType: .light, + backgroundColor: .white, + separatorColor: UIColor(white: 0.9, alpha: 1.0), + highlightedItemColor: UIColor(rgb: 0xe5e5ea), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0x5e5e5e), + accentColor: ACCENT_COLOR, + contrastColor: .green, + destructiveColor: UIColor(rgb: 0xff3b30), + disabledColor: UIColor(rgb: 0xd0d0d0), + controlBorderColor: .green, + baseFontSize: 17.0 + ), + actionSheet: ActionSheetControllerTheme( + dimColor: UIColor(white: 0.0, alpha: 0.4), + backgroundType: .light, + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), + standardActionTextColor: ACCENT_COLOR, + destructiveActionTextColor: UIColor(rgb: 0xff3b30), + disabledActionTextColor: UIColor(rgb: 0xb3b3b3), + primaryTextColor: .black, + secondaryTextColor: UIColor(rgb: 0x5e5e5e), + controlAccentColor: ACCENT_COLOR, + controlColor: UIColor(rgb: 0x7e8791), + switchFrameColor: UIColor(rgb: 0xe0e0e0), + switchContentColor: UIColor(rgb: 0x77d572), + switchHandleColor: UIColor(rgb: 0xffffff), + baseFontSize: 17.0 + ) +) diff --git a/Swiftgram/Playground/Sources/SwiftUIViewController.swift b/Swiftgram/Playground/Sources/SwiftUIViewController.swift new file mode 100644 index 00000000000..f4963692f0e --- /dev/null +++ b/Swiftgram/Playground/Sources/SwiftUIViewController.swift @@ -0,0 +1,256 @@ +import AsyncDisplayKit +import Display +import Foundation +import SwiftUI +import UIKit + +public class SwiftUIViewControllerInteraction { + let push: (ViewController) -> Void + let present: ( + _ controller: ViewController, + _ in: PresentationContextType, + _ with: ViewControllerPresentationArguments? + ) -> Void + let dismiss: (_ animated: Bool, _ completion: (() -> Void)?) -> Void + + init( + push: @escaping (ViewController) -> Void, + present: @escaping ( + _ controller: ViewController, + _ in: PresentationContextType, + _ with: ViewControllerPresentationArguments? + ) -> Void, + dismiss: @escaping (_ animated: Bool, _ completion: (() -> Void)?) -> Void + ) { + self.push = push + self.present = present + self.dismiss = dismiss + } +} + +public protocol SwiftUIView: View { + var controllerInteraction: SwiftUIViewControllerInteraction? { get set } + var navigationHeight: CGFloat { get set } +} + +struct MySwiftUIView: SwiftUIView { + var controllerInteraction: SwiftUIViewControllerInteraction? + @Binding var navigationHeight: CGFloat + + + var num: Int64 + + var body: some View { + Color.orange + .padding(.top, 2.0 * (_navigationHeight ?? 0)) + } +} + +struct CustomButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + .frame(height: 44) // Set a fixed height for all buttons + } +} + +private final class SwiftUIViewControllerNode: ASDisplayNode { + private let hostingController: UIHostingController + private var isDismissed = false + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + init(swiftUIView: Content) { + self.hostingController = UIHostingController(rootView: swiftUIView) + super.init() + + // For debugging + self.backgroundColor = .red.withAlphaComponent(0.3) + hostingController.view.backgroundColor = .blue.withAlphaComponent(0.3) + } + + override func didLoad() { + super.didLoad() + + // Defer the setup to ensure we have a valid view controller hierarchy + DispatchQueue.main.async { [weak self] in + self?.setupHostingController() + } + } + + private func setupHostingController() { + guard let viewController = findViewController() else { + assert(true, "Error: Could not find a parent view controller") + return + } + + viewController.addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: viewController) + + // Ensure the hosting controller's view has a size + hostingController.view.frame = self.bounds + + print("SwiftUIViewControllerNode setup - Node frame: \(self.frame), Hosting view frame: \(hostingController.view.frame)") + } + + private func findViewController() -> UIViewController? { + var responder: UIResponder? = self.view + while let nextResponder = responder?.next { + if let viewController = nextResponder as? UIViewController { + return viewController + } + responder = nextResponder + } + return nil + } + + override func layout() { + super.layout() + hostingController.view.frame = self.bounds + print("SwiftUIViewControllerNode layout - Node frame: \(self.frame), Hosting view frame: \(hostingController.view.frame)") + } + + func containerLayoutUpdated( + layout: ContainerViewLayout, + navigationHeight: CGFloat, + transition: ContainedViewLayoutTransition + ) { + if self.isDismissed { + return + } + + self.validLayout = (layout, navigationHeight) + + let frame = CGRect( + origin: CGPoint(x: 0, y: 0), + size: CGSize( + width: layout.size.width, + height: layout.size.height + ) + ) + + transition.updateFrame(node: self, frame: frame) + + print("containerLayoutUpdated - New frame: \(frame)") + + // Ensure hosting controller view is updated + hostingController.view.frame = bounds + hostingController.rootView.navigationHeight = navigationHeight + } + + func animateOut(completion: @escaping () -> Void) { + guard let (layout, navigationHeight) = validLayout else { + completion() + return + } + self.isDismissed = true + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + let frame = CGRect( + origin: CGPoint(x: 0, y: 0), + size: CGSize( + width: layout.size.width, + height: layout.size.height + ) + ) + + transition.updateFrame(node: self, frame: frame, completion: { _ in + completion() + }) + hostingController.rootView.navigationHeight = navigationHeight + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + print("SwiftUIViewControllerNode entered hierarchy") + } + + override func didExitHierarchy() { + super.didExitHierarchy() + hostingController.willMove(toParent: nil) + hostingController.view.removeFromSuperview() + hostingController.removeFromParent() + print("SwiftUIViewControllerNode exited hierarchy") + } +} + +public final class SwiftUIViewController: ViewController { + private var swiftUIView: Content + + public init( + _ swiftUIView: Content, + navigationBarTheme: NavigationBarTheme = NavigationBarTheme( + buttonColor: ACCENT_COLOR, + disabledButtonColor: .gray, + primaryTextColor: .black, + backgroundColor: .clear, + enableBackgroundBlur: true, + separatorColor: .gray, + badgeBackgroundColor: THEME.navigationBar.badgeBackgroundColor, + badgeStrokeColor: THEME.navigationBar.badgeStrokeColor, + badgeTextColor: THEME.navigationBar.badgeTextColor + ), + navigationBarStrings: NavigationBarStrings = NavigationBarStrings( + back: "Back", + close: "Close" + ) + ) { + self.swiftUIView = swiftUIView + super.init(navigationBarPresentationData: NavigationBarPresentationData( + theme: navigationBarTheme, + strings: navigationBarStrings + )) + + self.swiftUIView.controllerInteraction = SwiftUIViewControllerInteraction( + push: { [weak self] c in + guard let strongSelf = self else { return } + strongSelf.push(c) + }, + present: { [weak self] c, context, args in + guard let strongSelf = self else { return } + strongSelf.present(c, in: context, with: args) + }, + dismiss: { [weak self] animated, completion in + guard let strongSelf = self else { return } + strongSelf.dismiss(animated: animated, completion: completion) + } + ) + } + + @available(*, unavailable) + required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = SwiftUIViewControllerNode(swiftUIView: swiftUIView) + } + + override public func containerLayoutUpdated( + _ layout: ContainerViewLayout, + transition: ContainedViewLayoutTransition + ) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! SwiftUIViewControllerNode).containerLayoutUpdated( + layout: layout, + navigationHeight: navigationLayout(layout: layout).navigationFrame.maxY, + transition: transition + ) + } + + public func animateOut(completion: @escaping () -> Void) { + (self.displayNode as! SwiftUIViewControllerNode) + .animateOut(completion: completion) + } +} + + +func mySwiftUIViewController(_ num: Int64) -> ViewController { + let controller = SwiftUIViewController(MySwiftUIView(num: num)) + controller.title = "Controller: \(num)" + return controller +} diff --git a/Swiftgram/Playground/Sources/main.m b/Swiftgram/Playground/Sources/main.m new file mode 100644 index 00000000000..a63f787ddab --- /dev/null +++ b/Swiftgram/Playground/Sources/main.m @@ -0,0 +1,7 @@ +#import + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, @"Application", @"AppDelegate"); + } +} \ No newline at end of file diff --git a/Swiftgram/Playground/generate_project.py b/Swiftgram/Playground/generate_project.py new file mode 100755 index 00000000000..1124292600e --- /dev/null +++ b/Swiftgram/Playground/generate_project.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +from contextlib import contextmanager +import os +import subprocess +import sys +import shutil +import textwrap + +# Import the locate_bazel function +sys.path.append( + os.path.join(os.path.dirname(__file__), "..", "..", "build-system", "Make") +) +from BazelLocation import locate_bazel + + +@contextmanager +def cwd(path): + oldpwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(oldpwd) + + +def main(): + # Get the current script directory + current_script_dir = os.path.dirname(os.path.abspath(__file__)) + with cwd(os.path.join(current_script_dir, "..", "..")): + bazel_path = locate_bazel(os.getcwd()) + # 1. Kill all Xcode processes + subprocess.run(["killall", "Xcode"], check=False) + + # 2. Delete xcodeproj.bazelrc if it exists and write a new one + bazelrc_path = os.path.join(current_script_dir, "..", "..", "xcodeproj.bazelrc") + if os.path.exists(bazelrc_path): + os.remove(bazelrc_path) + + with open(bazelrc_path, "w") as f: + f.write( + textwrap.dedent( + """ + build --announce_rc + build --features=swift.use_global_module_cache + build --verbose_failures + build --features=swift.enable_batch_mode + build --features=-swift.debug_prefix_map + # build --disk_cache= + + build --swiftcopt=-no-warnings-as-errors + build --copt=-Wno-error + """ + ) + ) + + # 3. Delete the Xcode project if it exists + xcode_project_path = os.path.join(current_script_dir, "Playground.xcodeproj") + if os.path.exists(xcode_project_path): + shutil.rmtree(xcode_project_path) + + # 4. Write content to generate_project.py + generate_project_path = os.path.join(current_script_dir, "custom_bazel_path.bzl") + with open(generate_project_path, "w") as f: + f.write("def custom_bazel_path():\n") + f.write(f' return "{bazel_path}"\n') + + # 5. Run xcodeproj generator + working_dir = os.path.join(current_script_dir, "..", "..") + bazel_command = f'"{bazel_path}" run //Swiftgram/Playground:Playground_xcodeproj' + subprocess.run(bazel_command, shell=True, cwd=working_dir, check=True) + + # 5. Open Xcode project + subprocess.run(["open", xcode_project_path], check=True) + + +if __name__ == "__main__": + main() diff --git a/Swiftgram/Playground/launch_on_simulator.py b/Swiftgram/Playground/launch_on_simulator.py new file mode 100755 index 00000000000..feefa4f941f --- /dev/null +++ b/Swiftgram/Playground/launch_on_simulator.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +import subprocess +import json +import os +import time + + +def find_app(start_path): + for root, dirs, _ in os.walk(start_path): + for dir in dirs: + if dir.endswith(".app"): + return os.path.join(root, dir) + return None + + +def ensure_simulator_booted(device_name) -> str: + # List all devices + devices_json = subprocess.check_output( + ["xcrun", "simctl", "list", "devices", "--json"] + ).decode() + devices = json.loads(devices_json) + for runtime in devices["devices"]: + for device in devices["devices"][runtime]: + if device["name"] == device_name: + device_udid = device["udid"] + if device["state"] == "Booted": + print(f"Simulator {device_name} is already booted.") + return device_udid + break + if device_udid: + break + + if not device_udid: + raise Exception(f"Simulator {device_name} not found") + + # Boot the device + print(f"Booting simulator {device_name}...") + subprocess.run(["xcrun", "simctl", "boot", device_udid], check=True) + + # Wait for the device to finish booting + print("Waiting for simulator to finish booting...") + while True: + boot_status = subprocess.check_output( + ["xcrun", "simctl", "list", "devices"] + ).decode() + if f"{device_name} ({device_udid}) (Booted)" in boot_status: + break + time.sleep(0.5) + + print(f"Simulator {device_name} is now booted.") + return device_udid + + +def build_and_run_xcode_project(project_path, scheme_name, destination): + # Change to the directory containing the .xcodeproj file + os.chdir(os.path.dirname(project_path)) + + # Build the project + build_command = [ + "xcodebuild", + "-project", + project_path, + "-scheme", + scheme_name, + "-destination", + destination, + "-sdk", + "iphonesimulator", + "build", + ] + + try: + subprocess.run(build_command, check=True) + print("Build successful!") + except subprocess.CalledProcessError as e: + print(f"Build failed with error: {e}") + return + + # Get the bundle identifier and app path + settings_command = [ + "xcodebuild", + "-project", + project_path, + "-scheme", + scheme_name, + "-sdk", + "iphonesimulator", + "-showBuildSettings", + ] + + try: + result = subprocess.run( + settings_command, capture_output=True, text=True, check=True + ) + settings = result.stdout.split("\n") + bundle_id = next( + line.split("=")[1].strip() + for line in settings + if "PRODUCT_BUNDLE_IDENTIFIER" in line + ) + build_dir = next( + line.split("=")[1].strip() + for line in settings + if "TARGET_BUILD_DIR" in line + ) + + app_path = find_app(build_dir) + if not app_path: + print(f"Could not find .app file in {build_dir}") + return + print(f"Found app at: {app_path}") + print(f"Bundle identifier: {bundle_id}") + print(f"App path: {app_path}") + except (subprocess.CalledProcessError, StopIteration) as e: + print(f"Failed to get build settings: {e}") + return + + device_udid = ensure_simulator_booted(simulator_name) + + # Install the app on the simulator + install_command = ["xcrun", "simctl", "install", device_udid, app_path] + + try: + subprocess.run(install_command, check=True) + print("App installed on simulator successfully!") + except subprocess.CalledProcessError as e: + print(f"Failed to install app on simulator: {e}") + return + + # List installed apps + try: + listapps_cmd = "/usr/bin/xcrun simctl listapps booted | /usr/bin/plutil -convert json -r -o - -- -" + result = subprocess.run( + listapps_cmd, shell=True, capture_output=True, text=True, check=True + ) + apps = json.loads(result.stdout) + + if bundle_id in apps: + print(f"App {bundle_id} is installed on the simulator") + else: + print(f"App {bundle_id} is not installed on the simulator") + print("Installed apps:", list(apps.keys())) + except subprocess.CalledProcessError as e: + print(f"Failed to list apps: {e}") + except json.JSONDecodeError as e: + print(f"Failed to parse app list: {e}") + + # Focus simulator + subprocess.run(["open", "-a", "Simulator"], check=True) + + # Run the project on the simulator + run_command = ["xcrun", "simctl", "launch", "booted", bundle_id] + + try: + subprocess.run(run_command, check=True) + print("Application launched in simulator!") + except subprocess.CalledProcessError as e: + print(f"Failed to launch application in simulator: {e}") + + +# Usage +current_script_dir = os.path.dirname(os.path.abspath(__file__)) +project_path = os.path.join(current_script_dir, "Playground.xcodeproj") +scheme_name = "Playground" +simulator_name = "iPhone 15" +destination = f"platform=iOS Simulator,name={simulator_name},OS=latest" + +if __name__ == "__main__": + build_and_run_xcode_project(project_path, scheme_name, destination) diff --git a/Swiftgram/SFSafariViewControllerPlus/BUILD b/Swiftgram/SFSafariViewControllerPlus/BUILD new file mode 100644 index 00000000000..72a719f0b1e --- /dev/null +++ b/Swiftgram/SFSafariViewControllerPlus/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SFSafariViewControllerPlus", + module_name = "SFSafariViewControllerPlus", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift b/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift new file mode 100644 index 00000000000..1df3ddbaa33 --- /dev/null +++ b/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift @@ -0,0 +1,14 @@ +import SafariServices + +public class SFSafariViewControllerPlusDidFinish: SFSafariViewController, SFSafariViewControllerDelegate { + public var onDidFinish: (() -> Void)? + + public override init(url URL: URL, configuration: SFSafariViewController.Configuration = SFSafariViewController.Configuration()) { + super.init(url: URL, configuration: configuration) + self.delegate = self + } + + public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + onDidFinish?() + } +} diff --git a/Swiftgram/SGAPI/BUILD b/Swiftgram/SGAPI/BUILD new file mode 100644 index 00000000000..1a7634e2c8c --- /dev/null +++ b/Swiftgram/SGAPI/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPI", + module_name = "SGAPI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGWebAppExtensions:SGWebAppExtensions", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme", + "//Swiftgram/SGRegDateScheme:SGRegDateScheme", + "//Swiftgram/SGRequests:SGRequests", + "//Swiftgram/SGConfig:SGConfig" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPI/Sources/SGAPI.swift b/Swiftgram/SGAPI/Sources/SGAPI.swift new file mode 100644 index 00000000000..55114930404 --- /dev/null +++ b/Swiftgram/SGAPI/Sources/SGAPI.swift @@ -0,0 +1,145 @@ +import Foundation +import SwiftSignalKit + +import SGConfig +import SGLogging +import SGSimpleSettings +import SGWebAppExtensions +import SGWebSettingsScheme +import SGRequests +import SGRegDateScheme + +private let API_VERSION: String = "0" + +private func buildApiUrl(_ endpoint: String) -> String { + return "\(SG_CONFIG.apiUrl)/v\(API_VERSION)/\(endpoint)" +} + +public let SG_API_AUTHORIZATION_HEADER = "Authorization" +public let SG_API_DEVICE_TOKEN_HEADER = "Device-Token" + +private enum HTTPRequestError { + case network +} + +public enum SGAPIError { + case generic(String? = nil) +} + +public func getSGSettings(token: String) -> Signal { + return Signal { subscriber in + + let url = URL(string: buildApiUrl("settings"))! + let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"] + let completed = Atomic(value: false) + + var request = URLRequest(url: url) + headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + + let downloadSignal = requestsCustom(request: request).start(next: { data, urlResponse in + let _ = completed.swap(true) + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let settings = try decoder.decode(SGWebSettings.self, from: data) + subscriber.putNext(settings) + subscriber.putCompletion() + } catch { + subscriber.putError(.generic("Can't parse user settings: \(error). Response: \(String(data: data, encoding: .utf8) ?? "")")) + } + }, error: { error in + subscriber.putError(.generic("Error requesting user settings: \(String(describing: error))")) + }) + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadSignal.dispose() + } + } + } +} + + + +public func postSGSettings(token: String, data: [String:Any]) -> Signal { + return Signal { subscriber in + + let url = URL(string: buildApiUrl("settings"))! + let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"] + let completed = Atomic(value: false) + + var request = URLRequest(url: url) + headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + request.httpMethod = "POST" + + let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = jsonData + + let dataSignal = requestsCustom(request: request).start(next: { data, urlResponse in + let _ = completed.swap(true) + + if let httpResponse = urlResponse as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200...299: + subscriber.putCompletion() + default: + subscriber.putError(.generic("Can't update settings: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "")")) + } + } else { + subscriber.putError(.generic("Not an HTTP response: \(String(describing: urlResponse))")) + } + }, error: { error in + subscriber.putError(.generic("Error updating settings: \(String(describing: error))")) + }) + + return ActionDisposable { + if !completed.with({ $0 }) { + dataSignal.dispose() + } + } + } +} + +public func getSGAPIRegDate(token: String, deviceToken: String, userId: Int64) -> Signal { + return Signal { subscriber in + + let url = URL(string: buildApiUrl("regdate/\(userId)"))! + let headers = [ + SG_API_AUTHORIZATION_HEADER: "Token \(token)", + SG_API_DEVICE_TOKEN_HEADER: deviceToken + ] + let completed = Atomic(value: false) + + var request = URLRequest(url: url) + headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + request.timeoutInterval = 10 + + let downloadSignal = requestsCustom(request: request).start(next: { data, urlResponse in + let _ = completed.swap(true) + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let settings = try decoder.decode(RegDate.self, from: data) + subscriber.putNext(settings) + subscriber.putCompletion() + } catch { + subscriber.putError(.generic("Can't parse regDate: \(error). Response: \(String(data: data, encoding: .utf8) ?? "")")) + } + }, error: { error in + subscriber.putError(.generic("Error requesting regDate: \(String(describing: error))")) + }) + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadSignal.dispose() + } + } + } +} diff --git a/Swiftgram/SGAPIToken/BUILD b/Swiftgram/SGAPIToken/BUILD new file mode 100644 index 00000000000..9b507e1c2bf --- /dev/null +++ b/Swiftgram/SGAPIToken/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPIToken", + module_name = "SGAPIToken", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramCore:TelegramCore", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme", + "//Swiftgram/SGConfig:SGConfig", + "//Swiftgram/SGWebAppExtensions:SGWebAppExtensions", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPIToken/Sources/SGAPIToken.swift b/Swiftgram/SGAPIToken/Sources/SGAPIToken.swift new file mode 100644 index 00000000000..9ac4fff087d --- /dev/null +++ b/Swiftgram/SGAPIToken/Sources/SGAPIToken.swift @@ -0,0 +1,133 @@ +import Foundation +import SwiftSignalKit +import AccountContext +import TelegramCore +import SGLogging +import SGConfig +import SGWebAppExtensions + +private let tokenExpirationTime: TimeInterval = 30 * 60 // 30 minutes + +private var tokenCache: [Int64: (token: String, expiration: Date)] = [:] + +public enum SGAPITokenError { + case generic(String? = nil) +} + +public func getSGApiToken(context: AccountContext, botUsername: String = SG_CONFIG.botUsername) -> Signal { + let userId = context.account.peerId.id._internalGetInt64Value() + + if let (token, expiration) = tokenCache[userId], Date() < expiration { + // SGLogger.shared.log("SGAPI", "Using cached token. Expiring at: \(expiration)") + return Signal { subscriber in + subscriber.putNext(token) + subscriber.putCompletion() + return EmptyDisposable + } + } + + SGLogger.shared.log("SGAPI", "Requesting new token") + // Workaround for Apple Review + if context.account.testingEnvironment { + return context.account.postbox.transaction { transaction -> String? in + if let testUserPeer = transaction.getPeer(context.account.peerId) as? TelegramUser, let testPhone = testUserPeer.phone { + return testPhone + } else { + return nil + } + } + |> mapToSignalPromotingError { phone -> Signal in + if let phone = phone { + // https://core.telegram.org/api/auth#test-accounts + if phone.starts(with: String(99966)) { + SGLogger.shared.log("SGAPI", "Using demo token") + tokenCache[userId] = (phone, Date().addingTimeInterval(tokenExpirationTime)) + return .single(phone) + } else { + return .fail(.generic("Non-demo phone number on test DC")) + } + } else { + return .fail(.generic("Missing test account peer or it's number (how?)")) + } + } + } + + return Signal { subscriber in + let getSettingsURLSignal = getSGSettingsURL(context: context, botUsername: botUsername).start(next: { url in + if let hashPart = url.components(separatedBy: "#").last { + let parsedParams = urlParseHashParams(hashPart) + if let token = parsedParams["tgWebAppData"], let token = token { + tokenCache[userId] = (token, Date().addingTimeInterval(tokenExpirationTime)) + #if DEBUG + print("[SGAPI]", "API Token: \(token)") + #endif + subscriber.putNext(token) + subscriber.putCompletion() + } else { + subscriber.putError(.generic("Invalid or missing token in response url! \(url)")) + } + } else { + subscriber.putError(.generic("No hash part in URL \(url)")) + } + }) + + return ActionDisposable { + getSettingsURLSignal.dispose() + } + } +} + +public func getSGSettingsURL(context: AccountContext, botUsername: String = SG_CONFIG.botUsername, url: String = SG_CONFIG.webappUrl, themeParams: [String: Any]? = nil) -> Signal { + return Signal { subscriber in + // themeParams = generateWebAppThemeParams( + // context.sharedContext.currentPresentationData.with { $0 }.theme + // ) + var requestWebViewSignalDisposable: Disposable? = nil + var requestUpdatePeerIsBlocked: Disposable? = nil + let resolvePeerSignal = ( + context.engine.peers.resolvePeerByName(name: botUsername) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + }).start(next: { botPeer in + if let botPeer = botPeer { + SGLogger.shared.log("SGAPI", "Botpeer found for \(botUsername)") + let requestWebViewSignal = context.engine.messages.requestWebView(peerId: botPeer.id, botId: botPeer.id, url: url, payload: nil, themeParams: themeParams, fromMenu: true, replyToMessageId: nil, threadId: nil) + + requestWebViewSignalDisposable = requestWebViewSignal.start(next: { webViewResult in + subscriber.putNext(webViewResult.url) + subscriber.putCompletion() + }, error: { e in + SGLogger.shared.log("SGAPI", "Webview request error, retrying with unblock") + // if e.errorDescription == "YOU_BLOCKED_USER" { + requestUpdatePeerIsBlocked = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botPeer.id, isBlocked: false) + |> afterDisposed( + { + requestWebViewSignalDisposable?.dispose() + requestWebViewSignalDisposable = requestWebViewSignal.start(next: { webViewResult in + SGLogger.shared.log("SGAPI", "Webview retry success \(webViewResult)") + subscriber.putNext(webViewResult.url) + subscriber.putCompletion() + }, error: { e in + SGLogger.shared.log("SGAPI", "Webview retry failure \(e)") + subscriber.putError(.generic("Webview retry failure \(e)")) + }) + })).start() + // } + }) + + } else { + SGLogger.shared.log("SGAPI", "Botpeer not found for \(botUsername)") + subscriber.putError(.generic()) + } + }) + + return ActionDisposable { + resolvePeerSignal.dispose() + requestUpdatePeerIsBlocked?.dispose() + requestWebViewSignalDisposable?.dispose() + } + } +} diff --git a/Swiftgram/SGAPIWebSettings/BUILD b/Swiftgram/SGAPIWebSettings/BUILD new file mode 100644 index 00000000000..9964398d276 --- /dev/null +++ b/Swiftgram/SGAPIWebSettings/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPIWebSettings", + module_name = "SGAPIWebSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramCore:TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPIWebSettings/Sources/File.swift b/Swiftgram/SGAPIWebSettings/Sources/File.swift new file mode 100644 index 00000000000..9a10004f73c --- /dev/null +++ b/Swiftgram/SGAPIWebSettings/Sources/File.swift @@ -0,0 +1,49 @@ +import Foundation + +import SGAPIToken +import SGAPI +import SGLogging + +import AccountContext + +import SGSimpleSettings +import TelegramCore + +public func updateSGWebSettingsInteractivelly(context: AccountContext) { + let _ = getSGApiToken(context: context).startStandalone(next: { token in + let _ = getSGSettings(token: token).startStandalone(next: { webSettings in + SGLogger.shared.log("SGAPI", "New SGWebSettings for id \(context.account.peerId.id._internalGetInt64Value()): \(webSettings) ") + SGSimpleSettings.shared.canUseStealthMode = webSettings.global.storiesAvailable + let _ = (context.account.postbox.transaction { transaction in + updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in + var configuration = configuration + configuration.sgWebSettings = webSettings + return configuration + }) + }).startStandalone() + }, error: { e in + if case let .generic(errorMessage) = e, let errorMessage = errorMessage { + SGLogger.shared.log("SGAPI", errorMessage) + } + }) + }, error: { e in + if case let .generic(errorMessage) = e, let errorMessage = errorMessage { + SGLogger.shared.log("SGAPI", errorMessage) + } + }) +} + + +public func postSGWebSettingsInteractivelly(context: AccountContext, data: [String: Any]) { + let _ = getSGApiToken(context: context).startStandalone(next: { token in + let _ = postSGSettings(token: token, data: data).startStandalone(error: { e in + if case let .generic(errorMessage) = e, let errorMessage = errorMessage { + SGLogger.shared.log("SGAPI", errorMessage) + } + }) + }, error: { e in + if case let .generic(errorMessage) = e, let errorMessage = errorMessage { + SGLogger.shared.log("SGAPI", errorMessage) + } + }) +} diff --git a/Swiftgram/SGActionRequestHandlerSanitizer/BUILD b/Swiftgram/SGActionRequestHandlerSanitizer/BUILD new file mode 100644 index 00000000000..a27377792c7 --- /dev/null +++ b/Swiftgram/SGActionRequestHandlerSanitizer/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGActionRequestHandlerSanitizer", + module_name = "SGActionRequestHandlerSanitizer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift b/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift new file mode 100644 index 00000000000..f94edc1c686 --- /dev/null +++ b/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift @@ -0,0 +1,15 @@ +import Foundation + +public func sgActionRequestHandlerSanitizer(_ url: URL) -> URL { + var url = url + if let scheme = url.scheme { + let openInPrefix = "\(scheme)://parseurl?url=" + let urlString = url.absoluteString + if urlString.hasPrefix(openInPrefix) { + if let unwrappedUrlString = String(urlString.dropFirst(openInPrefix.count)).removingPercentEncoding, let newUrl = URL(string: unwrappedUrlString) { + url = newUrl + } + } + } + return url +} diff --git a/Swiftgram/SGConfig/BUILD b/Swiftgram/SGConfig/BUILD new file mode 100644 index 00000000000..68f53fc3113 --- /dev/null +++ b/Swiftgram/SGConfig/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGConfig", + module_name = "SGConfig", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/BuildConfig:BuildConfig" + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGConfig/Sources/File.swift b/Swiftgram/SGConfig/Sources/File.swift new file mode 100644 index 00000000000..3a2a4265688 --- /dev/null +++ b/Swiftgram/SGConfig/Sources/File.swift @@ -0,0 +1,20 @@ +import Foundation +import BuildConfig + +public struct SGConfig: Codable { + public var apiUrl: String = "https://api.swiftgram.app" + public var webappUrl: String = "https://my.swiftgram.app" + public var botUsername: String = "SwiftgramBot" +} + +private func parseSGConfig(_ jsonString: String) -> SGConfig { + let jsonData = Data(jsonString.utf8) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return (try? decoder.decode(SGConfig.self, from: jsonData)) ?? SGConfig() +} + +private let baseAppBundleId = Bundle.main.bundleIdentifier! +private let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId) +public let SG_CONFIG: SGConfig = parseSGConfig(buildConfig.sgConfig) +public let SG_API_WEBAPP_URL_PARSED = URL(string: SG_CONFIG.webappUrl)! \ No newline at end of file diff --git a/Swiftgram/SGContentAnalysis/BUILD b/Swiftgram/SGContentAnalysis/BUILD new file mode 100644 index 00000000000..8679395f707 --- /dev/null +++ b/Swiftgram/SGContentAnalysis/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGContentAnalysis", + module_name = "SGContentAnalysis", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift b/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift new file mode 100644 index 00000000000..b75ba3fd3e4 --- /dev/null +++ b/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift @@ -0,0 +1,64 @@ +import SensitiveContentAnalysis +import SwiftSignalKit + +public enum ContentAnalysisError: Error { + case generic(_ message: String) +} + +public enum ContentAnalysisMediaType { + case image + case video +} + +public func canAnalyzeMedia() -> Bool { + if #available(iOS 17, *) { + let analyzer = SCSensitivityAnalyzer() + let policy = analyzer.analysisPolicy + return policy != .disabled + } else { + return false + } +} + + +public func analyzeMediaSignal(_ url: URL, mediaType: ContentAnalysisMediaType = .image) -> Signal { + return Signal { subscriber in + analyzeMedia(url: url, mediaType: mediaType, completion: { result, error in + if let result = result { + subscriber.putNext(result) + subscriber.putCompletion() + } else if let error = error { + subscriber.putError(error) + } else { + subscriber.putError(ContentAnalysisError.generic("Unknown response")) + } + }) + + return ActionDisposable { + } + } +} + +private func analyzeMedia(url: URL, mediaType: ContentAnalysisMediaType, completion: @escaping (Bool?, Error?) -> Void) { + if #available(iOS 17, *) { + let analyzer = SCSensitivityAnalyzer() + switch mediaType { + case .image: + analyzer.analyzeImage(at: url) { analysisResult, analysisError in + completion(analysisResult?.isSensitive, analysisError) + } + case .video: + Task { + do { + let handler = analyzer.videoAnalysis(forFileAt: url) + let response = try await handler.hasSensitiveContent() + completion(response.isSensitive, nil) + } catch { + completion(nil, error) + } + } + } + } else { + completion(false, nil) + } +} diff --git a/Swiftgram/SGDBReset/BUILD b/Swiftgram/SGDBReset/BUILD new file mode 100644 index 00000000000..c9e2113bd6f --- /dev/null +++ b/Swiftgram/SGDBReset/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGDBReset", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGDBReset/Sources/File.swift b/Swiftgram/SGDBReset/Sources/File.swift new file mode 100644 index 00000000000..1029c5e1529 --- /dev/null +++ b/Swiftgram/SGDBReset/Sources/File.swift @@ -0,0 +1,47 @@ +import UIKit +import Foundation +import SGLogging + +private let dbResetKey = "sg_db_reset" + +public func sgDBResetIfNeeded(databasePath: String, present: ((UIViewController) -> ())?) { + guard UserDefaults.standard.bool(forKey: dbResetKey) else { + return + } + NSLog("[SG.DBReset] Resetting DB with system settings") + let alert = UIAlertController( + title: "Database reset.\nPlease wait...", + message: nil, + preferredStyle: .alert + ) + present?(alert) + do { + let _ = try FileManager.default.removeItem(atPath: databasePath) + NSLog("[SG.DBReset] Done. Reset completed") + let successAlert = UIAlertController( + title: "Database reset completed", + message: nil, + preferredStyle: .alert + ) + successAlert.addAction(UIAlertAction(title: "Restart App", style: .cancel) { _ in + exit(0) + }) + successAlert.addAction(UIAlertAction(title: "OK", style: .default)) + alert.dismiss(animated: false) { + present?(successAlert) + } + } catch { + NSLog("[SG.DBReset] ERROR. Failed to reset database: \(error)") + let failAlert = UIAlertController( + title: "ERROR. Failed to reset database", + message: "\(error)", + preferredStyle: .alert + ) + alert.dismiss(animated: false) { + present?(failAlert) + } + } + UserDefaults.standard.set(false, forKey: dbResetKey) +// let semaphore = DispatchSemaphore(value: 0) +// semaphore.wait() +} diff --git a/Swiftgram/SGDebugUI/BUILD b/Swiftgram/SGDebugUI/BUILD new file mode 100644 index 00000000000..e5f93c9f803 --- /dev/null +++ b/Swiftgram/SGDebugUI/BUILD @@ -0,0 +1,46 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +config_setting( + name = "debug_build", + values = { + "compilation_mode": "dbg", + }, +) + +flex_dependency = select({ + ":debug_build": [ + "@flex_sdk//:FLEX" + ], + "//conditions:default": [], +}) + + +swift_library( + name = "SGDebugUI", + module_name = "SGDebugUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGItemListUI:SGItemListUI", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGStrings:SGStrings", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/Display:Display", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/UndoUI:UndoUI", + ] + flex_dependency, + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift new file mode 100644 index 00000000000..45905ead93a --- /dev/null +++ b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift @@ -0,0 +1,115 @@ +import Foundation +import SGItemListUI +import UndoUI +import AccountContext +import Display +import TelegramCore +import ItemListUI +import SwiftSignalKit +import TelegramPresentationData +import PresentationDataUtils + +// Optional +import SGSimpleSettings +import SGLogging +import OverlayStatusController +#if DEBUG +import FLEX +#endif + +private enum SGDebugControllerSection: Int32, SGItemListSection { + case base +} + +private enum SGDebugActions: String { + case flexing + case clearRegDateCache +} + +private enum SGDebugToggles: String { + case forceImmediateShareSheet +} + + +private typealias SGDebugControllerEntry = SGItemListUIEntry + +private func SGDebugControllerEntries(presentationData: PresentationData) -> [SGDebugControllerEntry] { + var entries: [SGDebugControllerEntry] = [] + + let id = SGItemListCounter() + #if DEBUG + entries.append(.action(id: id.count, section: .base, actionType: .flexing, text: "FLEX", kind: .generic)) + #endif + entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic)) + entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true)) + + return entries +} +private func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController { + return UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }) +} + + +public func sgDebugController(context: AccountContext) -> ViewController { + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + + let simplePromise = ValuePromise(true, ignoreRepeated: false) + + let arguments = SGItemListArguments(context: context, setBoolValue: { toggleName, value in + switch toggleName { + case .forceImmediateShareSheet: + SGSimpleSettings.shared.forceSystemSharing = value + } + }, action: { actionType in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch actionType { + case .clearRegDateCache: + SGLogger.shared.log("SGDebug", "Regdate cache cleanup init") + + /* + let spinner = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + + presentControllerImpl?(spinner, nil) + */ + SGSimpleSettings.shared.regDateCache.drop() + SGLogger.shared.log("SGDebug", "Regdate cache cleanup succesfull") + presentControllerImpl?(okUndoController("OK: Regdate cache cleaned", presentationData), nil) + /* + Queue.mainQueue().async() { [weak spinner] in + spinner?.dismiss() + } + */ + case .flexing: + #if DEBUG + FLEXManager.shared.toggleExplorer() + #endif + } + }) + + let signal = combineLatest(context.sharedContext.presentationData, simplePromise.get()) + |> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in + + let entries = SGDebugControllerEntries(presentationData: presentationData) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ ) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + // Workaround + let _ = pushControllerImpl + + return controller +} + + diff --git a/Swiftgram/SGDeviceToken/BUILD b/Swiftgram/SGDeviceToken/BUILD new file mode 100644 index 00000000000..8a1446f3f1f --- /dev/null +++ b/Swiftgram/SGDeviceToken/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGDeviceToken", + module_name = "SGDeviceToken", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGDeviceToken/Sources/File.swift b/Swiftgram/SGDeviceToken/Sources/File.swift new file mode 100644 index 00000000000..abf7df33570 --- /dev/null +++ b/Swiftgram/SGDeviceToken/Sources/File.swift @@ -0,0 +1,31 @@ +import SwiftSignalKit +import DeviceCheck + +public enum SGDeviceTokenError { + case unsupportedDevice + case generic(String) +} + +public func getDeviceToken() -> Signal { + return Signal { subscriber in + let currentDevice = DCDevice.current + if currentDevice.isSupported { + currentDevice.generateToken { (data, error) in + guard error == nil else { + subscriber.putError(.generic(error!.localizedDescription)) + return + } + if let tokenData = data { + subscriber.putNext(tokenData.base64EncodedString()) + subscriber.putCompletion() + } else { + subscriber.putError(.generic("Empty Token")) + } + } + } else { + subscriber.putError(.unsupportedDevice) + } + return ActionDisposable { + } + } +} diff --git a/Swiftgram/SGDoubleTapMessageAction/BUILD b/Swiftgram/SGDoubleTapMessageAction/BUILD new file mode 100644 index 00000000000..ac9be00d708 --- /dev/null +++ b/Swiftgram/SGDoubleTapMessageAction/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGDoubleTapMessageAction", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift b/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift new file mode 100644 index 00000000000..2cefa9b8473 --- /dev/null +++ b/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift @@ -0,0 +1,13 @@ +import Foundation +import SGSimpleSettings +import Postbox +import TelegramCore + + +func sgDoubleTapMessageAction(incoming: Bool, message: Message) -> String { + if incoming { + return SGSimpleSettings.MessageDoubleTapAction.default.rawValue + } else { + return SGSimpleSettings.shared.messageDoubleTapActionOutgoing + } +} diff --git a/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD b/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD new file mode 100644 index 00000000000..87428676030 --- /dev/null +++ b/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGEmojiKeyboardDefaultFirst", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift b/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift new file mode 100644 index 00000000000..8d582084e29 --- /dev/null +++ b/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift @@ -0,0 +1,23 @@ +import Foundation + + +func sgPatchEmojiKeyboardItems(_ items: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] { + var items = items + let staticEmojisIndex = items.firstIndex { item in + if let groupId = item.groupId.base as? String, groupId == "static" { + return true + } + return false + } + let recentEmojisIndex = items.firstIndex { item in + if let groupId = item.groupId.base as? String, groupId == "recent" { + return true + } + return false + } + if let staticEmojisIndex = staticEmojisIndex { + let staticEmojiItem = items.remove(at: staticEmojisIndex) + items.insert(staticEmojiItem, at: (recentEmojisIndex ?? -1) + 1 ) + } + return items +} \ No newline at end of file diff --git a/Swiftgram/SGItemListUI/BUILD b/Swiftgram/SGItemListUI/BUILD new file mode 100644 index 00000000000..d0dd4589861 --- /dev/null +++ b/Swiftgram/SGItemListUI/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGItemListUI", + module_name = "SGItemListUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/MtProtoKit:MtProtoKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift b/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift new file mode 100644 index 00000000000..78d802eac2d --- /dev/null +++ b/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift @@ -0,0 +1,333 @@ +// MARK: Swiftgram +import SGLogging +import SGSimpleSettings +import SGStrings +import SGAPIToken + +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import MtProtoKit +import MessageUI +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import AppBundle +import WebKit +import PeerNameColorScreen + +public class SGItemListCounter { + private var _count = 0 + + public init() {} + + public var count: Int { + _count += 1 + return _count + } + + public func increment(_ amount: Int) { + _count += amount + } + + public func countWith(_ amount: Int) -> Int { + _count += amount + return count + } +} + + +public protocol SGItemListSection: Equatable { + var rawValue: Int32 { get } +} + +public final class SGItemListArguments { + let context: AccountContext + // + let setBoolValue: (BoolSetting, Bool) -> Void + let updateSliderValue: (SliderSetting, Int32) -> Void + let setOneFromManyValue: (OneFromManySetting) -> Void + let openDisclosureLink: (DisclosureLink) -> Void + let action: (ActionType) -> Void + let searchInput: (String) -> Void + + + public init( + context: AccountContext, + // + setBoolValue: @escaping (BoolSetting, Bool) -> Void = { _,_ in }, + updateSliderValue: @escaping (SliderSetting, Int32) -> Void = { _,_ in }, + setOneFromManyValue: @escaping (OneFromManySetting) -> Void = { _ in }, + openDisclosureLink: @escaping (DisclosureLink) -> Void = { _ in}, + action: @escaping (ActionType) -> Void = { _ in }, + searchInput: @escaping (String) -> Void = { _ in } + ) { + self.context = context + // + self.setBoolValue = setBoolValue + self.updateSliderValue = updateSliderValue + self.setOneFromManyValue = setOneFromManyValue + self.openDisclosureLink = openDisclosureLink + self.action = action + self.searchInput = searchInput + } +} + +public enum SGItemListUIEntry: ItemListNodeEntry { + case header(id: Int, section: Section, text: String, badge: String?) + case toggle(id: Int, section: Section, settingName: BoolSetting, value: Bool, text: String, enabled: Bool) + case notice(id: Int, section: Section, text: String) + case percentageSlider(id: Int, section: Section, settingName: SliderSetting, value: Int32) + case oneFromManySelector(id: Int, section: Section, settingName: OneFromManySetting, text: String, value: String, enabled: Bool) + case disclosure(id: Int, section: Section, link: DisclosureLink, text: String) + case peerColorDisclosurePreview(id: Int, section: Section, name: String, color: UIColor) + case action(id: Int, section: Section, actionType: ActionType, text: String, kind: ItemListActionKind) + case searchInput(id: Int, section: Section, title: NSAttributedString, text: String, placeholder: String) + + public var section: ItemListSectionId { + switch self { + case let .header(_, sectionId, _, _): + return sectionId.rawValue + case let .toggle(_, sectionId, _, _, _, _): + return sectionId.rawValue + case let .notice(_, sectionId, _): + return sectionId.rawValue + + case let .disclosure(_, sectionId, _, _): + return sectionId.rawValue + + case let .percentageSlider(_, sectionId, _, _): + return sectionId.rawValue + + case let .peerColorDisclosurePreview(_, sectionId, _, _): + return sectionId.rawValue + case let .oneFromManySelector(_, sectionId, _, _, _, _): + return sectionId.rawValue + + case let .action(_, sectionId, _, _, _): + return sectionId.rawValue + + case let .searchInput(_, sectionId, _, _, _): + return sectionId.rawValue + } + } + + public var stableId: Int { + switch self { + case let .header(stableIdValue, _, _, _): + return stableIdValue + case let .toggle(stableIdValue, _, _, _, _, _): + return stableIdValue + case let .notice(stableIdValue, _, _): + return stableIdValue + case let .disclosure(stableIdValue, _, _, _): + return stableIdValue + case let .percentageSlider(stableIdValue, _, _, _): + return stableIdValue + case let .peerColorDisclosurePreview(stableIdValue, _, _, _): + return stableIdValue + case let .oneFromManySelector(stableIdValue, _, _, _, _, _): + return stableIdValue + case let .action(stableIdValue, _, _, _, _): + return stableIdValue + case let .searchInput(stableIdValue, _, _, _, _): + return stableIdValue + } + } + + public static func <(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + public static func ==(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool { + switch (lhs, rhs) { + case let (.header(id1, section1, text1, badge1), .header(id2, section2, text2, badge2)): + return id1 == id2 && section1 == section2 && text1 == text2 && badge1 == badge2 + + case let (.toggle(id1, section1, settingName1, value1, text1, enabled1), .toggle(id2, section2, settingName2, value2, text2, enabled2)): + return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2 && text1 == text2 && enabled1 == enabled2 + + case let (.notice(id1, section1, text1), .notice(id2, section2, text2)): + return id1 == id2 && section1 == section2 && text1 == text2 + + case let (.percentageSlider(id1, section1, settingName1, value1), .percentageSlider(id2, section2, settingName2, value2)): + return id1 == id2 && section1 == section2 && value1 == value2 && settingName1 == settingName2 + + case let (.disclosure(id1, section1, link1, text1), .disclosure(id2, section2, link2, text2)): + return id1 == id2 && section1 == section2 && link1 == link2 && text1 == text2 + + case let (.peerColorDisclosurePreview(id1, section1, name1, currentColor1), .peerColorDisclosurePreview(id2, section2, name2, currentColor2)): + return id1 == id2 && section1 == section2 && name1 == name2 && currentColor1 == currentColor2 + + case let (.oneFromManySelector(id1, section1, settingName1, text1, value1, enabled1), .oneFromManySelector(id2, section2, settingName2, text2, value2, enabled2)): + return id1 == id2 && section1 == section2 && settingName1 == settingName2 && text1 == text2 && value1 == value2 && enabled1 == enabled2 + case let (.action(id1, section1, actionType1, text1, kind1), .action(id2, section2, actionType2, text2, kind2)): + return id1 == id2 && section1 == section2 && actionType1 == actionType2 && text1 == text2 && kind1 == kind2 + + case let (.searchInput(id1, lhsValue1, lhsValue2, lhsValue3, lhsValue4), .searchInput(id2, rhsValue1, rhsValue2, rhsValue3, rhsValue4)): + return id1 == id2 && lhsValue1 == rhsValue1 && lhsValue2 == rhsValue2 && lhsValue3 == rhsValue3 && lhsValue4 == rhsValue4 + + default: + return false + } + } + + + public func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! SGItemListArguments + switch self { + case let .header(_, _, string, badge): + return ItemListSectionHeaderItem(presentationData: presentationData, text: string, badge: badge, sectionId: self.section) + + case let .toggle(_, _, setting, value, text, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.setBoolValue(setting, value) + }) + case let .notice(_, _, string): + return ItemListTextItem(presentationData: presentationData, text: .markdown(string), sectionId: self.section) + case let .disclosure(_, _, link, text): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks) { + arguments.openDisclosureLink(link) + } + case let .percentageSlider(_, _, setting, value): + return SliderPercentageItem( + theme: presentationData.theme, + strings: presentationData.strings, + value: value, + sectionId: self.section, + updated: { value in + arguments.updateSliderValue(setting, value) + } + ) + + case let .peerColorDisclosurePreview(_, _, name, color): + return ItemListDisclosureItem(presentationData: presentationData, title: " ", enabled: false, label: name, labelStyle: .semitransparentBadge(color), centerLabelAlignment: true, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + }) + + case let .oneFromManySelector(_, _, settingName, text, value, enabled): + return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, sectionId: self.section, style: .blocks, action: { + arguments.setOneFromManyValue(settingName) + }) + case let .action(_, _, actionType, text, kind): + return ItemListActionItem(presentationData: presentationData, title: text, kind: kind, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.action(actionType) + }) + case let .searchInput(_, _, title, text, placeholder): + return ItemListSingleLineInputItem(presentationData: presentationData, title: title, text: text, placeholder: placeholder, returnKeyType: .done, spacing: 3.0, clearType: .always, selectAllOnFocus: true, secondaryStyle: true, sectionId: self.section, textUpdated: { input in arguments.searchInput(input) }, action: {}, dismissKeyboardOnEnter: true) + } + } +} + + +public func filterSGItemListUIEntrires( + entries: [SGItemListUIEntry], + by searchQuery: String? +) -> [SGItemListUIEntry] { + + guard let query = searchQuery?.lowercased(), !query.isEmpty else { + return entries + } + + var sectionIdsForEntireIncludion: Set = [] + var sectionIdsWithMatches: Set = [] + var filteredEntries: [SGItemListUIEntry] = [] + + func entryMatches(_ entry: SGItemListUIEntry, query: String) -> Bool { + switch entry { + case .header(_, _, let text, _): + return text.lowercased().contains(query) + case .toggle(_, _, _, _, let text, _): + return text.lowercased().contains(query) + case .notice(_, _, let text): + return text.lowercased().contains(query) + case .percentageSlider: + return false // Assuming percentage sliders don't have searchable text + case .oneFromManySelector(_, _, _, let text, let value, _): + return text.lowercased().contains(query) || value.lowercased().contains(query) + case .disclosure(_, _, _, let text): + return text.lowercased().contains(query) + case .peerColorDisclosurePreview: + return false // Never indexed during search + case .action(_, _, _, let text, _): + return text.lowercased().contains(query) + case .searchInput: + return true // Never hiding search input + } + } + + // First pass: identify sections with matches + for entry in entries { + if entryMatches(entry, query: query) { + switch entry { + case .searchInput: + continue + default: + sectionIdsWithMatches.insert(entry.section) + } + } + } + + // Second pass: keep matching entries and headers of sections with matches + for (index, entry) in entries.enumerated() { + switch entry { + case .header: + if entryMatches(entry, query: query) { + // Will show all entries for the same section + sectionIdsForEntireIncludion.insert(entry.section) + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + // Or show header if something from the section already matched + if sectionIdsWithMatches.contains(entry.section) { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + default: + if entryMatches(entry, query: query) { + if case .notice = entry { + // add previous entry to if it's not another notice and if it's not already here + // possibly targeting related toggle / setting if we've matched it's description (notice) in search + if index > 0 { + let previousEntry = entries[index - 1] + if case .notice = previousEntry {} else { + if !filteredEntries.contains(previousEntry) { + filteredEntries.append(previousEntry) + } + } + } + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } else { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + // add next entry if it's notice + // possibly targeting description (notice) for the currently search-matched toggle/setting + if index < entries.count - 1 { + let nextEntry = entries[index + 1] + if case .notice = nextEntry { + if !filteredEntries.contains(nextEntry) { + filteredEntries.append(nextEntry) + } + } + } + } + } else if sectionIdsForEntireIncludion.contains(entry.section) { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + } + } + + return filteredEntries +} diff --git a/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift b/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift new file mode 100644 index 00000000000..ad61f47bba7 --- /dev/null +++ b/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift @@ -0,0 +1,353 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import LegacyComponents +import ItemListUI +import PresentationDataUtils +import AppBundle + +public class SliderPercentageItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + public let sectionId: ItemListSectionId + let updated: (Int32) -> Void + + public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.value = value + self.sectionId = sectionId + self.updated = updated + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = SliderPercentageItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SliderPercentageItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private func rescalePercentageValueToSlider(_ value: CGFloat) -> CGFloat { + return max(0.0, min(1.0, value)) +} + +private func rescaleSliderValueToPercentageValue(_ value: CGFloat) -> CGFloat { + return max(0.0, min(1.0, value)) +} + +class SliderPercentageItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private var sliderView: TGPhotoEditorSliderView? + private let leftTextNode: ImmediateTextNode + private let rightTextNode: ImmediateTextNode + private let centerTextNode: ImmediateTextNode + private let centerMeasureTextNode: ImmediateTextNode + + private let batteryImage: UIImage? + private let batteryBackgroundNode: ASImageNode + private let batteryForegroundNode: ASImageNode + + private var item: SliderPercentageItem? + private var layoutParams: ListViewItemLayoutParams? + + // MARK: Swiftgram + private let activateArea: AccessibilityAreaNode + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.leftTextNode = ImmediateTextNode() + self.rightTextNode = ImmediateTextNode() + self.centerTextNode = ImmediateTextNode() + self.centerMeasureTextNode = ImmediateTextNode() + + self.batteryImage = nil //UIImage(bundleImageName: "Settings/UsageBatteryFrame") + self.batteryBackgroundNode = ASImageNode() + self.batteryForegroundNode = ASImageNode() + + // MARK: Swiftgram + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.leftTextNode) + self.addSubnode(self.rightTextNode) + self.addSubnode(self.centerTextNode) + self.addSubnode(self.batteryBackgroundNode) + self.addSubnode(self.batteryForegroundNode) + self.addSubnode(self.activateArea) + + // MARK: Swiftgram + self.activateArea.increment = { [weak self] in + if let self { + self.sliderView?.increase(by: 0.10) + } + } + + self.activateArea.decrement = { [weak self] in + if let self { + self.sliderView?.decrease(by: 0.10) + } + } + } + + override func didLoad() { + super.didLoad() + + let sliderView = TGPhotoEditorSliderView() + sliderView.enableEdgeTap = true + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 1.0 + sliderView.lineSize = 4.0 + sliderView.minimumValue = 0.0 + sliderView.startValue = 0.0 + sliderView.maximumValue = 1.0 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.displayEdges = true + if let item = self.item, let params = self.layoutParams { + sliderView.value = rescalePercentageValueToSlider(CGFloat(item.value) / 100.0) + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) + } + self.view.addSubview(sliderView) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: SliderPercentageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 88.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + strongSelf.leftTextNode.attributedText = NSAttributedString(string: "0%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) + strongSelf.rightTextNode.attributedText = NSAttributedString(string: "100%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) + + let centralText: String = "\(item.value)%" + let centralMeasureText: String = centralText + strongSelf.batteryBackgroundNode.isHidden = true + strongSelf.batteryForegroundNode.isHidden = strongSelf.batteryBackgroundNode.isHidden + strongSelf.centerTextNode.attributedText = NSAttributedString(string: centralText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor) + strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centralMeasureText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor) + + strongSelf.leftTextNode.isAccessibilityElement = true + strongSelf.leftTextNode.accessibilityLabel = "Minimum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.minimumValue ?? 0.0) * 100.0))%" + strongSelf.rightTextNode.isAccessibilityElement = true + strongSelf.rightTextNode.accessibilityLabel = "Maximum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.maximumValue ?? 1.0) * 100.0))%" + + let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + + let sideInset: CGFloat = 18.0 + + strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize) + strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize) + + var centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0), size: centerTextSize) + if !strongSelf.batteryBackgroundNode.isHidden { + centerFrame.origin.x -= 12.0 + } + strongSelf.centerTextNode.frame = centerFrame + + if let frameImage = strongSelf.batteryImage { + strongSelf.batteryBackgroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: frameImage, color: item.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.9)) { + image.draw(in: CGRect(origin: CGPoint(), size: size)) + + let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0)) + context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath) + context.clip() + } + + UIGraphicsPopContext() + }) + strongSelf.batteryForegroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + + let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0)) + context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath) + context.clip() + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width * CGFloat(item.value) / 100.0, height: contentRect.height)), cornerRadius: 1.0).cgPath) + context.fillPath() + + UIGraphicsPopContext() + }) + + let batteryColor: UIColor + if item.value <= 20 { + batteryColor = UIColor(rgb: 0xFF3B30) + } else { + batteryColor = item.theme.list.itemSwitchColors.positiveColor + } + + if strongSelf.batteryForegroundNode.layer.layerTintColor == nil { + strongSelf.batteryForegroundNode.layer.layerTintColor = batteryColor.cgColor + } else { + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTintColor(layer: strongSelf.batteryForegroundNode.layer, color: batteryColor) + } + + strongSelf.batteryBackgroundNode.frame = CGRect(origin: CGPoint(x: centerFrame.minX + centerMeasureTextSize.width + 4.0, y: floor(centerFrame.midY - frameImage.size.height * 0.5)), size: frameImage.size) + strongSelf.batteryForegroundNode.frame = strongSelf.batteryBackgroundNode.frame + } + + if let sliderView = strongSelf.sliderView { + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSecondaryTextColor + sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45) + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + } + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) + } + + strongSelf.activateArea.accessibilityLabel = "Slider" + strongSelf.activateArea.accessibilityValue = centralMeasureText + strongSelf.activateArea.accessibilityTraits = .adjustable + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func sliderValueChanged() { + guard let sliderView = self.sliderView else { + return + } + self.item?.updated(Int32(rescaleSliderValueToPercentageValue(sliderView.value) * 100.0)) + } +} + diff --git a/Swiftgram/SGLogging/BUILD b/Swiftgram/SGLogging/BUILD new file mode 100644 index 00000000000..498396974c4 --- /dev/null +++ b/Swiftgram/SGLogging/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGLogging", + module_name = "SGLogging", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ManagedFile:ManagedFile" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGLogging/Sources/SGLogger.swift b/Swiftgram/SGLogging/Sources/SGLogger.swift new file mode 100644 index 00000000000..22a88f02a81 --- /dev/null +++ b/Swiftgram/SGLogging/Sources/SGLogger.swift @@ -0,0 +1,236 @@ +import Foundation +import SwiftSignalKit +import ManagedFile + +private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility) + +private var sharedLogger: SGLogger? + +private let binaryEventMarker: UInt64 = 0xcadebabef00dcafe + +private func rootPathForBasePath(_ appGroupPath: String) -> String { + return appGroupPath + "/telegram-data" +} + +public final class SGLogger { + private let queue = Queue(name: "app.swiftgram.ios.log", qos: .utility) + private let maxLength: Int = 2 * 1024 * 1024 + private let maxShortLength: Int = 1 * 1024 * 1024 + private let maxFiles: Int = 20 + + private let rootPath: String + private let basePath: String + private var file: (ManagedFile, Int)? + private var shortFile: (ManagedFile, Int)? + + public static let sgLogsPath = "/logs/app-logs-sg" + + public var logToFile: Bool = true + public var logToConsole: Bool = true + public var redactSensitiveData: Bool = true + + public static func setSharedLogger(_ logger: SGLogger) { + sharedLogger = logger + } + + public static var shared: SGLogger { + if let sharedLogger = sharedLogger { + return sharedLogger + } else { + print("SGLogger setup...") + guard let baseAppBundleId = Bundle.main.bundleIdentifier else { + print("Can't setup logger (1)!") + return SGLogger(rootPath: "", basePath: "") + } + let appGroupName = "group.\(baseAppBundleId)" + let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) + guard let appGroupUrl = maybeAppGroupUrl else { + print("Can't setup logger (2)!") + return SGLogger(rootPath: "", basePath: "") + } + let newRootPath = rootPathForBasePath(appGroupUrl.path) + let newLogsPath = newRootPath + sgLogsPath + let _ = try? FileManager.default.createDirectory(atPath: newLogsPath, withIntermediateDirectories: true, attributes: nil) + self.setSharedLogger(SGLogger(rootPath: newRootPath, basePath: newLogsPath)) + if let sharedLogger = sharedLogger { + return sharedLogger + } else { + print("Can't setup logger (3)!") + return SGLogger(rootPath: "", basePath: "") + } + } + } + + public init(rootPath: String, basePath: String) { + self.rootPath = rootPath + self.basePath = basePath + } + + public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> { + return Signal { subscriber in + self.queue.async { + let logsPath: String + if let prefix = prefix { + logsPath = self.rootPath + prefix + } else { + logsPath = self.basePath + } + + var result: [(Date, String, String)] = [] + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + for url in files { + if url.lastPathComponent.hasPrefix("log-") { + if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate { + result.append((creationDate, url.lastPathComponent, url.path)) + } + } + } + } + result.sort(by: { $0.0 < $1.0 }) + subscriber.putNext(result.map { ($0.1, $0.2) }) + subscriber.putCompletion() + } + + return EmptyDisposable + } + } + + public func collectLogs(basePath: String) -> Signal<[(String, String)], NoError> { + return Signal { subscriber in + self.queue.async { + let logsPath: String = basePath + + var result: [(Date, String, String)] = [] + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + for url in files { + if url.lastPathComponent.hasPrefix("log-") { + if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate { + result.append((creationDate, url.lastPathComponent, url.path)) + } + } + } + } + result.sort(by: { $0.0 < $1.0 }) + subscriber.putNext(result.map { ($0.1, $0.2) }) + subscriber.putCompletion() + } + + return EmptyDisposable + } + } + + public func log(_ tag: String, _ what: @autoclosure () -> String) { + if !self.logToFile && !self.logToConsole { + return + } + + let string = what() + + var rawTime = time_t() + time(&rawTime) + var timeinfo = tm() + localtime_r(&rawTime, &timeinfo) + + var curTime = timeval() + gettimeofday(&curTime, nil) + let milliseconds = curTime.tv_usec / 1000 + + var consoleContent: String? + if self.logToConsole { + let content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string]) + consoleContent = content + print(content) + } + + if self.logToFile { + self.queue.async { + let content: String + if let consoleContent = consoleContent { + content = consoleContent + } else { + content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string]) + } + + var currentFile: ManagedFile? + var openNew = false + if let (file, length) = self.file { + if length >= self.maxLength { + self.file = nil + openNew = true + } else { + currentFile = file + } + } else { + openNew = true + } + if openNew { + let _ = try? FileManager.default.createDirectory(atPath: self.basePath, withIntermediateDirectories: true, attributes: nil) + + var createNew = false + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + var minCreationDate: (Date, URL)? + var maxCreationDate: (Date, URL)? + var count = 0 + for url in files { + if url.lastPathComponent.hasPrefix("log-") { + if let values = try? url.resourceValues(forKeys: Set([URLResourceKey.creationDateKey])), let creationDate = values.creationDate { + count += 1 + if minCreationDate == nil || minCreationDate!.0 > creationDate { + minCreationDate = (creationDate, url) + } + if maxCreationDate == nil || maxCreationDate!.0 < creationDate { + maxCreationDate = (creationDate, url) + } + } + } + } + if let (_, url) = minCreationDate, count >= self.maxFiles { + let _ = try? FileManager.default.removeItem(at: url) + } + if let (_, url) = maxCreationDate { + var value = stat() + if stat(url.path, &value) == 0 && Int(value.st_size) < self.maxLength { + if let file = ManagedFile(queue: self.queue, path: url.path, mode: .append) { + self.file = (file, Int(value.st_size)) + currentFile = file + } + } else { + createNew = true + } + } else { + createNew = true + } + } + + if createNew { + let fileName = String(format: "log-%d-%d-%d_%02d-%02d-%02d.%03d.txt", arguments: [Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds)]) + + let path = self.basePath + "/" + fileName + + if let file = ManagedFile(queue: self.queue, path: path, mode: .append) { + self.file = (file, 0) + currentFile = file + } + } + } + + if let currentFile = currentFile { + if let data = content.data(using: .utf8) { + data.withUnsafeBytes { rawBytes -> Void in + let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + + let _ = currentFile.write(bytes, count: data.count) + } + var newline: UInt8 = 0x0a + let _ = currentFile.write(&newline, count: 1) + if let file = self.file { + self.file = (file.0, file.1 + data.count + 1) + } else { + assertionFailure() + } + } + } + } + } + } +} diff --git a/Swiftgram/SGLogging/Sources/Utils.swift b/Swiftgram/SGLogging/Sources/Utils.swift new file mode 100644 index 00000000000..68381110b15 --- /dev/null +++ b/Swiftgram/SGLogging/Sources/Utils.swift @@ -0,0 +1,6 @@ +//import Foundation +// +//public func extractNameFromPath(_ path: String) -> String { +// let fileName = URL(fileURLWithPath: path).lastPathComponent +// return String(fileName.prefix(upTo: fileName.lastIndex { $0 == "." } ?? fileName.endIndex)) +//} diff --git a/Swiftgram/SGRegDate/BUILD b/Swiftgram/SGRegDate/BUILD new file mode 100644 index 00000000000..ff5f233e309 --- /dev/null +++ b/Swiftgram/SGRegDate/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRegDate", + module_name = "SGRegDate", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGRegDateScheme:SGRegDateScheme", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGDeviceToken:SGDeviceToken", + "//Swiftgram/SGStrings:SGStrings", + + "//submodules/AccountContext:AccountContext", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRegDate/Sources/SGRegDate.swift b/Swiftgram/SGRegDate/Sources/SGRegDate.swift new file mode 100644 index 00000000000..0be00196836 --- /dev/null +++ b/Swiftgram/SGRegDate/Sources/SGRegDate.swift @@ -0,0 +1,45 @@ +import Foundation +import SwiftSignalKit +import TelegramPresentationData + +import SGLogging +import SGStrings +import SGRegDateScheme +import AccountContext +import SGSimpleSettings +import SGAPI +import SGAPIToken +import SGDeviceToken + +public enum RegDateError { + case generic +} + +public func getRegDate(context: AccountContext, peerId: Int64) -> Signal { + return Signal { subscriber in + var tokensRequestSignal: Disposable? = nil + var apiRequestSignal: Disposable? = nil + if let regDateData = SGSimpleSettings.shared.regDateCache[String(peerId)], let regDate = try? JSONDecoder().decode(RegDate.self, from: regDateData), regDate.validUntil == 0 || regDate.validUntil > Int64(Date().timeIntervalSince1970) { + subscriber.putNext(regDate) + subscriber.putCompletion() + } else if SGSimpleSettings.shared.showRegDate { + tokensRequestSignal = combineLatest(getDeviceToken() |> mapError { error -> Void in SGLogger.shared.log("SGDeviceToken", "Error generating token: \(error)"); return Void() } , getSGApiToken(context: context) |> mapError { _ -> Void in return Void() }).start(next: { deviceToken, apiToken in + apiRequestSignal = getSGAPIRegDate(token: apiToken, deviceToken: deviceToken, userId: peerId).start(next: { regDate in + if let data = try? JSONEncoder().encode(regDate) { + SGSimpleSettings.shared.regDateCache[String(peerId)] = data + } + subscriber.putNext(regDate) + subscriber.putCompletion() + }) + }) + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + + return ActionDisposable { + tokensRequestSignal?.dispose() + apiRequestSignal?.dispose() + } + } +} diff --git a/Swiftgram/SGRegDateScheme/BUILD b/Swiftgram/SGRegDateScheme/BUILD new file mode 100644 index 00000000000..008f82658db --- /dev/null +++ b/Swiftgram/SGRegDateScheme/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRegDateScheme", + module_name = "SGRegDateScheme", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRegDateScheme/Sources/File.swift b/Swiftgram/SGRegDateScheme/Sources/File.swift new file mode 100644 index 00000000000..a972377e8bb --- /dev/null +++ b/Swiftgram/SGRegDateScheme/Sources/File.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct RegDate: Codable { + public let from: Int64 + public let to: Int64 + public let validUntil: Int64 +} diff --git a/Swiftgram/SGRequests/BUILD b/Swiftgram/SGRequests/BUILD new file mode 100644 index 00000000000..979d84f32e9 --- /dev/null +++ b/Swiftgram/SGRequests/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRequests", + module_name = "SGRequests", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit" + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRequests/Sources/File.swift b/Swiftgram/SGRequests/Sources/File.swift new file mode 100644 index 00000000000..19dfa3da279 --- /dev/null +++ b/Swiftgram/SGRequests/Sources/File.swift @@ -0,0 +1,72 @@ +import Foundation +import SwiftSignalKit + + +public func requestsDownload(url: URL) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + + let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, response, error in + let _ = completed.swap(true) + if let location = location, let data = try? Data(contentsOf: location) { + subscriber.putNext((data, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + downloadTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadTask.cancel() + } + } + } +} + +public func requestsGet(url: URL) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + + let urlTask = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in + let _ = completed.swap(true) + if let strongData = data { + subscriber.putNext((strongData, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + urlTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + urlTask.cancel() + } + } + } +} + + +public func requestsCustom(request: URLRequest) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + let urlTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in + _ = completed.swap(true) + if let strongData = data { + subscriber.putNext((strongData, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + urlTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + urlTask.cancel() + } + } + } +} diff --git a/Swiftgram/SGSettingsBundle/BUILD b/Swiftgram/SGSettingsBundle/BUILD new file mode 100644 index 00000000000..e0d37a3c515 --- /dev/null +++ b/Swiftgram/SGSettingsBundle/BUILD @@ -0,0 +1,10 @@ +load("@build_bazel_rules_apple//apple:resources.bzl", "apple_bundle_import") + +apple_bundle_import( + name = "SGSettingsBundle", + bundle_imports = glob([ + "Settings.bundle/*", + "Settings.bundle/**/*", + ]), + visibility = ["//visibility:public"] +) \ No newline at end of file diff --git a/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist b/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist new file mode 100644 index 00000000000..92c85b662db --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist @@ -0,0 +1,29 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + FooterText + Reset.Notice + Title + Reset.Title + + + Type + PSToggleSwitchSpecifier + Title + Reset.Toggle + Key + sg_db_reset + DefaultValue + + + + + diff --git a/Swiftgram/SGSettingsBundle/Settings.bundle/en.lproj/Root.strings b/Swiftgram/SGSettingsBundle/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 00000000000..6986865c883 --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/en.lproj/Root.strings @@ -0,0 +1,5 @@ +/* A single strings file, whose title is specified in your preferences schema. The strings files provide the localized content to display to the user for each of your preferences. */ + +"Reset.Title" = "TROUBLESHOOTING"; +"Reset.Toggle" = "Reset caches on next launch"; +"Reset.Notice" = "Use in case you're stuck and can't open the app. This WILL NOT logout your accounts, but all secret chats will be lost."; diff --git a/Swiftgram/SGSettingsBundle/Settings.bundle/ru.lproj/Root.strings b/Swiftgram/SGSettingsBundle/Settings.bundle/ru.lproj/Root.strings new file mode 100644 index 00000000000..42015a1b91c --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/ru.lproj/Root.strings @@ -0,0 +1,3 @@ +"Reset.Title" = "РЕШЕНИЕ ПРОБЛЕМ"; +"Reset.Toggle" = "Сбросить кэш при следующем запуске"; +"Reset.Notice" = "Используйте, если приложение вылетает или не загружается. Эта опция НЕ СБРАСЫВАЕТ ваши аккаунты, но удалит все секретные чаты."; diff --git a/Swiftgram/SGSettingsUI/BUILD b/Swiftgram/SGSettingsUI/BUILD new file mode 100644 index 00000000000..dc1613e7810 --- /dev/null +++ b/Swiftgram/SGSettingsUI/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +filegroup( + name = "SGUIAssets", + srcs = glob(["Images.xcassets/**"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "SGSettingsUI", + module_name = "SGSettingsUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGItemListUI:SGItemListUI", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGStrings:SGStrings", +# "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/MtProtoKit:MtProtoKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", + "//submodules/UndoUI:UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json new file mode 100644 index 00000000000..526cf46d7c8 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lt_savetocloud.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf new file mode 100644 index 00000000000..ed4efd9629f Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf differ diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json new file mode 100644 index 00000000000..6fb419fc51b --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "swiftgram_context_menu.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf new file mode 100644 index 00000000000..30789ecb778 Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf differ diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json new file mode 100644 index 00000000000..1bf20b6bc87 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "Swiftgram.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } + } + \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf new file mode 100644 index 00000000000..6abd681bf69 Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf differ diff --git a/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift new file mode 100644 index 00000000000..d213ffb1b84 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift @@ -0,0 +1,666 @@ +// MARK: Swiftgram +import SGLogging +import SGSimpleSettings +import SGStrings +import SGAPIToken + +import SGItemListUI +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import MtProtoKit +import MessageUI +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import OverlayStatusController +import AccountContext +import AppBundle +import WebKit +import PeerNameColorScreen +import UndoUI + + +private enum SGControllerSection: Int32, SGItemListSection { + case search + case content + case tabs + case folders + case chatList + case profiles + case stories + case translation + case photo + case stickers + case videoNotes + case contextMenu + case accountColors + case other +} + +private enum SGBoolSetting: String { + case hidePhoneInSettings + case showTabNames + case showContactsTab + case showCallsTab + case foldersAtBottom + case startTelescopeWithRearCam + case hideStories + case uploadSpeedBoost + case showProfileId + case warnOnStoriesOpen + case sendWithReturnKey + case rememberLastFolder + case sendLargePhotos + case storyStealthMode + case disableSwipeToRecordStory + case disableDeleteChatSwipeOption + case quickTranslateButton + case hideReactions + case showRepostToStory + case contextShowSelectFromUser + case contextShowSaveToCloud + case contextShowHideForwardName + case contextShowRestrict + case contextShowReport + case contextShowReply + case contextShowPin + case contextShowSaveMedia + case contextShowMessageReplies + case contextShowJson + case disableScrollToNextChannel + case disableScrollToNextTopic + case disableChatSwipeOptions + case disableGalleryCamera + case disableSendAsButton + case disableSnapDeletionEffect + case stickerTimestamp + case hideRecordingButton + case hideTabBar + case showDC + case showCreationDate + case showRegDate + case compactChatList + case compactFolderNames + case allChatsHidden + case defaultEmojisFirst + case messageDoubleTapActionOutgoingEdit + case wideChannelPosts + case forceEmojiTab + case forceBuiltInMic + case hideChannelBottomButton + case confirmCalls +} + +private enum SGOneFromManySetting: String { + case bottomTabStyle + case downloadSpeedBoost + case allChatsTitleLengthOverride +// case allChatsFolderPositionOverride +} + +private enum SGSliderSetting: String { + case accountColorsSaturation + case outgoingPhotoQuality + case stickerSize +} + +private enum SGDisclosureLink: String { + case contentSettings + case languageSettings +} + +private struct PeerNameColorScreenState: Equatable { + var updatedNameColor: PeerNameColor? + var updatedBackgroundEmojiId: Int64? +} + +private struct SGSettingsControllerState: Equatable { + var searchQuery: String? +} + +private typealias SGControllerEntry = SGItemListUIEntry + +private func SGControllerEntries(presentationData: PresentationData, callListSettings: CallListSettings, experimentalUISettings: ExperimentalUISettings, SGSettings: SGUISettings, appConfiguration: AppConfiguration, nameColors: PeerNameColors, state: SGSettingsControllerState) -> [SGControllerEntry] { + + let lang = presentationData.strings.baseLanguageCode + var entries: [SGControllerEntry] = [] + + let id = SGItemListCounter() + + entries.append(.searchInput(id: id.count, section: .search, title: NSAttributedString(string: "🔍"), text: state.searchQuery ?? "", placeholder: presentationData.strings.Common_Search)) + if appConfiguration.sgWebSettings.global.canEditSettings { + entries.append(.disclosure(id: id.count, section: .content, link: .contentSettings, text: i18n("Settings.ContentSettings", lang))) + } else { + id.increment(1) + } + + entries.append(.header(id: id.count, section: .tabs, text: i18n("Settings.Tabs.Header", lang), badge: nil)) + entries.append(.toggle(id: id.count, section: .tabs, settingName: .hideTabBar, value: SGSimpleSettings.shared.hideTabBar, text: i18n("Settings.Tabs.HideTabBar", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .tabs, settingName: .showContactsTab, value: callListSettings.showContactsTab, text: i18n("Settings.Tabs.ShowContacts", lang), enabled: !SGSimpleSettings.shared.hideTabBar)) + entries.append(.toggle(id: id.count, section: .tabs, settingName: .showCallsTab, value: callListSettings.showTab, text: presentationData.strings.CallSettings_TabIcon, enabled: !SGSimpleSettings.shared.hideTabBar)) + entries.append(.toggle(id: id.count, section: .tabs, settingName: .showTabNames, value: SGSimpleSettings.shared.showTabNames, text: i18n("Settings.Tabs.ShowNames", lang), enabled: !SGSimpleSettings.shared.hideTabBar)) + + entries.append(.header(id: id.count, section: .folders, text: presentationData.strings.Settings_ChatFolders.uppercased(), badge: nil)) + entries.append(.toggle(id: id.count, section: .folders, settingName: .foldersAtBottom, value: experimentalUISettings.foldersTabAtBottom, text: i18n("Settings.Folders.BottomTab", lang), enabled: true)) + entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .bottomTabStyle, text: i18n("Settings.Folders.BottomTabStyle", lang), value: i18n("Settings.Folders.BottomTabStyle.\(SGSimpleSettings.shared.bottomTabStyle)", lang), enabled: experimentalUISettings.foldersTabAtBottom)) + entries.append(.toggle(id: id.count, section: .folders, settingName: .allChatsHidden, value: SGSimpleSettings.shared.allChatsHidden, text: i18n("Settings.Folders.AllChatsHidden", lang, presentationData.strings.ChatList_Tabs_AllChats), enabled: true)) + #if DEBUG +// entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsFolderPositionOverride, text: i18n("Settings.Folders.AllChatsPlacement", lang), value: i18n("Settings.Folders.AllChatsPlacement.\(SGSimpleSettings.shared.allChatsFolderPositionOverride)", lang), enabled: true)) + #endif + entries.append(.toggle(id: id.count, section: .folders, settingName: .compactFolderNames, value: SGSimpleSettings.shared.compactFolderNames, text: i18n("Settings.Folders.CompactNames", lang), enabled: SGSimpleSettings.shared.bottomTabStyle != SGSimpleSettings.BottomTabStyleValues.ios.rawValue)) + entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsTitleLengthOverride, text: i18n("Settings.Folders.AllChatsTitle", lang), value: i18n("Settings.Folders.AllChatsTitle.\(SGSimpleSettings.shared.allChatsTitleLengthOverride)", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .folders, settingName: .rememberLastFolder, value: SGSimpleSettings.shared.rememberLastFolder, text: i18n("Settings.Folders.RememberLast", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .folders, text: i18n("Settings.Folders.RememberLast.Notice", lang))) + + entries.append(.header(id: id.count, section: .chatList, text: i18n("Settings.ChatList.Header", lang), badge: nil)) + entries.append(.toggle(id: id.count, section: .chatList, settingName: .compactChatList, value: SGSimpleSettings.shared.compactChatList, text: i18n("Settings.CompactChatList", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableChatSwipeOptions, value: !SGSimpleSettings.shared.disableChatSwipeOptions, text: i18n("Settings.ChatSwipeOptions", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableDeleteChatSwipeOption, value: !SGSimpleSettings.shared.disableDeleteChatSwipeOption, text: i18n("Settings.DeleteChatSwipeOption", lang), enabled: !SGSimpleSettings.shared.disableChatSwipeOptions)) + + entries.append(.header(id: id.count, section: .profiles, text: i18n("Settings.Profiles.Header", lang), badge: nil)) + entries.append(.toggle(id: id.count, section: .profiles, settingName: .showProfileId, value: SGSettings.showProfileId, text: i18n("Settings.ShowProfileID", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .profiles, settingName: .showDC, value: SGSimpleSettings.shared.showDC, text: i18n("Settings.ShowDC", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .profiles, settingName: .showRegDate, value: SGSimpleSettings.shared.showRegDate, text: i18n("Settings.ShowRegDate", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowRegDate.Notice", lang))) + entries.append(.toggle(id: id.count, section: .profiles, settingName: .showCreationDate, value: SGSimpleSettings.shared.showCreationDate, text: i18n("Settings.ShowCreationDate", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowCreationDate.Notice", lang))) + entries.append(.toggle(id: id.count, section: .profiles, settingName: .confirmCalls, value: SGSimpleSettings.shared.confirmCalls, text: i18n("Settings.CallConfirmation", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.CallConfirmation.Notice", lang))) + + entries.append(.header(id: id.count, section: .stories, text: presentationData.strings.AutoDownloadSettings_Stories.uppercased(), badge: nil)) + entries.append(.toggle(id: id.count, section: .stories, settingName: .hideStories, value: SGSettings.hideStories, text: i18n("Settings.Stories.Hide", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .stories, settingName: .disableSwipeToRecordStory, value: SGSimpleSettings.shared.disableSwipeToRecordStory, text: i18n("Settings.Stories.DisableSwipeToRecord", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .stories, settingName: .warnOnStoriesOpen, value: SGSettings.warnOnStoriesOpen, text: i18n("Settings.Stories.WarnBeforeView", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .stories, settingName: .showRepostToStory, value: SGSimpleSettings.shared.showRepostToStory, text: presentationData.strings.Share_RepostToStory.replacingOccurrences(of: "\n", with: " "), enabled: true)) + if SGSimpleSettings.shared.canUseStealthMode { + entries.append(.toggle(id: id.count, section: .stories, settingName: .storyStealthMode, value: SGSimpleSettings.shared.storyStealthMode, text: presentationData.strings.Story_StealthMode_Title, enabled: true)) + entries.append(.notice(id: id.count, section: .stories, text: presentationData.strings.Story_StealthMode_ControlText)) + } else { + id.increment(2) + } + + + entries.append(.header(id: id.count, section: .translation, text: presentationData.strings.Localization_TranslateMessages.uppercased(), badge: nil)) + entries.append(.toggle(id: id.count, section: .translation, settingName: .quickTranslateButton, value: SGSimpleSettings.shared.quickTranslateButton, text: i18n("Settings.Translation.QuickTranslateButton", lang), enabled: true)) + entries.append(.disclosure(id: id.count, section: .translation, link: .languageSettings, text: presentationData.strings.Localization_TranslateEntireChat)) + entries.append(.notice(id: id.count, section: .translation, text: i18n("Common.NoTelegramPremiumNeeded", lang, presentationData.strings.Settings_Premium))) + + entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.NetworkUsageSettings_MediaImageDataSection, badge: nil)) + entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.PhotoEditor_QualityTool.uppercased(), badge: nil)) + entries.append(.percentageSlider(id: id.count, section: .photo, settingName: .outgoingPhotoQuality, value: SGSimpleSettings.shared.outgoingPhotoQuality)) + entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.Quality.Notice", lang))) + entries.append(.toggle(id: id.count, section: .photo, settingName: .sendLargePhotos, value: SGSimpleSettings.shared.sendLargePhotos, text: i18n("Settings.Photo.SendLarge", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.SendLarge.Notice", lang))) + + entries.append(.header(id: id.count, section: .stickers, text: presentationData.strings.StickerPacksSettings_Title.uppercased(), badge: nil)) + entries.append(.header(id: id.count, section: .stickers, text: i18n("Settings.Stickers.Size", lang), badge: nil)) + entries.append(.percentageSlider(id: id.count, section: .stickers, settingName: .stickerSize, value: SGSimpleSettings.shared.stickerSize)) + entries.append(.toggle(id: id.count, section: .stickers, settingName: .stickerTimestamp, value: SGSimpleSettings.shared.stickerTimestamp, text: i18n("Settings.Stickers.Timestamp", lang), enabled: true)) + + + entries.append(.header(id: id.count, section: .videoNotes, text: i18n("Settings.VideoNotes.Header", lang), badge: nil)) + entries.append(.toggle(id: id.count, section: .videoNotes, settingName: .startTelescopeWithRearCam, value: SGSimpleSettings.shared.startTelescopeWithRearCam, text: i18n("Settings.VideoNotes.StartWithRearCam", lang), enabled: true)) + + entries.append(.header(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu", lang), badge: nil)) + entries.append(.notice(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu.Notice", lang))) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveToCloud, value: SGSimpleSettings.shared.contextShowSaveToCloud, text: i18n("ContextMenu.SaveToCloud", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowHideForwardName, value: SGSimpleSettings.shared.contextShowHideForwardName, text: presentationData.strings.Conversation_ForwardOptions_HideSendersNames, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSelectFromUser, value: SGSimpleSettings.shared.contextShowSelectFromUser, text: i18n("ContextMenu.SelectFromUser", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReport, value: SGSimpleSettings.shared.contextShowReport, text: presentationData.strings.Conversation_ContextMenuReport, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReply, value: SGSimpleSettings.shared.contextShowReply, text: presentationData.strings.Conversation_ContextMenuReply, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowPin, value: SGSimpleSettings.shared.contextShowPin, text: presentationData.strings.Conversation_Pin, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveMedia, value: SGSimpleSettings.shared.contextShowSaveMedia, text: presentationData.strings.Conversation_SaveToFiles, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowMessageReplies, value: SGSimpleSettings.shared.contextShowMessageReplies, text: presentationData.strings.Conversation_ContextViewThread, enabled: true)) + entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowJson, value: SGSimpleSettings.shared.contextShowJson, text: "JSON", enabled: true)) + /* entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan)) */ + + entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Header", lang), badge: nil)) + entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation", lang), badge: nil)) + let accountColorSaturation = SGSimpleSettings.shared.accountColorsSaturation + entries.append(.percentageSlider(id: id.count, section: .accountColors, settingName: .accountColorsSaturation, value: accountColorSaturation)) +// let nameColor: PeerNameColor +// if let updatedNameColor = state.updatedNameColor { +// nameColor = updatedNameColor +// } else { +// nameColor = .blue +// } +// let _ = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance) +// entries.append(.peerColorPicker(id: entries.count, section: .other, +// colors: nameColors, +// currentColor: nameColor, // TODO: PeerNameColor(rawValue: <#T##Int32#>) +// currentSaturation: accountColorSaturation +// )) + + if accountColorSaturation == 0 { + id.increment(100) + entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: presentationData.theme.chat.message.incoming.accentTextColor)) + } else { + id.increment(200) + for index in nameColors.displayOrder.prefix(3) { + let color: PeerNameColor = PeerNameColor(rawValue: index) + let colors = nameColors.get(color, dark: presentationData.theme.overallDarkAppearance) + entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: colors.main)) + } + } + entries.append(.notice(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation.Notice", lang))) + + id.increment(10000) + entries.append(.header(id: id.count, section: .other, text: presentationData.strings.Appearance_Other.uppercased(), badge: nil)) + entries.append(.toggle(id: id.count, section: .other, settingName: .hideChannelBottomButton, value: SGSimpleSettings.shared.hideChannelBottomButton, text: i18n("Settings.hideChannelBottomButton", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .wideChannelPosts, value: SGSimpleSettings.shared.wideChannelPosts, text: i18n("Settings.wideChannelPosts", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .forceBuiltInMic, value: SGSimpleSettings.shared.forceBuiltInMic, text: i18n("Settings.forceBuiltInMic", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.forceBuiltInMic.Notice", lang))) + entries.append(.toggle(id: id.count, section: .other, settingName: .messageDoubleTapActionOutgoingEdit, value: SGSimpleSettings.shared.messageDoubleTapActionOutgoing == SGSimpleSettings.MessageDoubleTapAction.edit.rawValue, text: i18n("Settings.messageDoubleTapActionOutgoingEdit", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .hideRecordingButton, value: !SGSimpleSettings.shared.hideRecordingButton, text: i18n("Settings.RecordingButton", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .disableSnapDeletionEffect, value: !SGSimpleSettings.shared.disableSnapDeletionEffect, text: i18n("Settings.SnapDeletionEffect", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .disableSendAsButton, value: !SGSimpleSettings.shared.disableSendAsButton, text: i18n("Settings.SendAsButton", lang, presentationData.strings.Conversation_SendMesageAs), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .disableGalleryCamera, value: !SGSimpleSettings.shared.disableGalleryCamera, text: i18n("Settings.GalleryCamera", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextChannel, value: !SGSimpleSettings.shared.disableScrollToNextChannel, text: i18n("Settings.PullToNextChannel", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextTopic, value: !SGSimpleSettings.shared.disableScrollToNextTopic, text: i18n("Settings.PullToNextTopic", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .hideReactions, value: SGSimpleSettings.shared.hideReactions, text: i18n("Settings.HideReactions", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .uploadSpeedBoost, value: SGSimpleSettings.shared.uploadSpeedBoost, text: i18n("Settings.UploadsBoost", lang), enabled: true)) + entries.append(.oneFromManySelector(id: id.count, section: .other, settingName: .downloadSpeedBoost, text: i18n("Settings.DownloadsBoost", lang), value: i18n("Settings.DownloadsBoost.\(SGSimpleSettings.shared.downloadSpeedBoost)", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .sendWithReturnKey, value: SGSettings.sendWithReturnKey, text: i18n("Settings.SendWithReturnKey", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .forceEmojiTab, value: SGSimpleSettings.shared.forceEmojiTab, text: i18n("Settings.ForceEmojiTab", lang), enabled: true)) + entries.append(.toggle(id: id.count, section: .other, settingName: .defaultEmojisFirst, value: SGSimpleSettings.shared.defaultEmojisFirst, text: i18n("Settings.DefaultEmojisFirst", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.DefaultEmojisFirst.Notice", lang))) + entries.append(.toggle(id: id.count, section: .other, settingName: .hidePhoneInSettings, value: SGSimpleSettings.shared.hidePhoneInSettings, text: i18n("Settings.HidePhoneInSettingsUI", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.HidePhoneInSettingsUI.Notice", lang))) + + return filterSGItemListUIEntrires(entries: entries, by: state.searchQuery) +} + +public func sgSettingsController(context: AccountContext/*, focusOnItemTag: Int? = nil*/) -> ViewController { + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? +// var getRootControllerImpl: (() -> UIViewController?)? +// var getNavigationControllerImpl: (() -> NavigationController?)? + var askForRestart: (() -> Void)? + + let initialState = SGSettingsControllerState() + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((SGSettingsControllerState) -> SGSettingsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + +// let sliderPromise = ValuePromise(SGSimpleSettings.shared.accountColorsSaturation, ignoreRepeated: true) +// let sliderStateValue = Atomic(value: SGSimpleSettings.shared.accountColorsSaturation) +// let _: ((Int32) -> Int32) -> Void = { f in +// sliderPromise.set(sliderStateValue.modify( {f($0)})) +// } + + let simplePromise = ValuePromise(true, ignoreRepeated: false) + + let arguments = SGItemListArguments( + context: context, + /*updatePeerColor: { color in + updateState { state in + var updatedState = state + updatedState.updatedNameColor = color + return updatedState + } + },*/ setBoolValue: { setting, value in + switch setting { + case .hidePhoneInSettings: + SGSimpleSettings.shared.hidePhoneInSettings = value + askForRestart?() + case .showTabNames: + SGSimpleSettings.shared.showTabNames = value + askForRestart?() + case .showContactsTab: + let _ = ( + updateCallListSettingsInteractively( + accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowContactsTab(value) } + ) + ).start() + case .showCallsTab: + let _ = ( + updateCallListSettingsInteractively( + accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowTab(value) } + ) + ).start() + case .foldersAtBottom: + let _ = ( + updateExperimentalUISettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in + var settings = settings + settings.foldersTabAtBottom = value + return settings + } + ) + ).start() + case .startTelescopeWithRearCam: + SGSimpleSettings.shared.startTelescopeWithRearCam = value + case .hideStories: + let _ = ( + updateSGUISettings(engine: context.engine, { settings in + var settings = settings + settings.hideStories = value + return settings + }) + ).start() + case .showProfileId: + let _ = ( + updateSGUISettings(engine: context.engine, { settings in + var settings = settings + settings.showProfileId = value + return settings + }) + ).start() + case .warnOnStoriesOpen: + let _ = ( + updateSGUISettings(engine: context.engine, { settings in + var settings = settings + settings.warnOnStoriesOpen = value + return settings + }) + ).start() + case .sendWithReturnKey: + let _ = ( + updateSGUISettings(engine: context.engine, { settings in + var settings = settings + settings.sendWithReturnKey = value + return settings + }) + ).start() + case .rememberLastFolder: + SGSimpleSettings.shared.rememberLastFolder = value + case .sendLargePhotos: + SGSimpleSettings.shared.sendLargePhotos = value + case .storyStealthMode: + SGSimpleSettings.shared.storyStealthMode = value + case .disableSwipeToRecordStory: + SGSimpleSettings.shared.disableSwipeToRecordStory = value + case .quickTranslateButton: + SGSimpleSettings.shared.quickTranslateButton = value + case .uploadSpeedBoost: + SGSimpleSettings.shared.uploadSpeedBoost = value + case .hideReactions: + SGSimpleSettings.shared.hideReactions = value + case .showRepostToStory: + SGSimpleSettings.shared.showRepostToStory = value + case .contextShowSelectFromUser: + SGSimpleSettings.shared.contextShowSelectFromUser = value + case .contextShowSaveToCloud: + SGSimpleSettings.shared.contextShowSaveToCloud = value + case .contextShowRestrict: + SGSimpleSettings.shared.contextShowRestrict = value + case .contextShowHideForwardName: + SGSimpleSettings.shared.contextShowHideForwardName = value + case .disableScrollToNextChannel: + SGSimpleSettings.shared.disableScrollToNextChannel = !value + case .disableScrollToNextTopic: + SGSimpleSettings.shared.disableScrollToNextTopic = !value + case .disableChatSwipeOptions: + SGSimpleSettings.shared.disableChatSwipeOptions = !value + simplePromise.set(true) // Trigger update for 'enabled' field of other toggles + askForRestart?() + case .disableDeleteChatSwipeOption: + SGSimpleSettings.shared.disableDeleteChatSwipeOption = !value + askForRestart?() + case .disableGalleryCamera: + SGSimpleSettings.shared.disableGalleryCamera = !value + case .disableSendAsButton: + SGSimpleSettings.shared.disableSendAsButton = !value + case .disableSnapDeletionEffect: + SGSimpleSettings.shared.disableSnapDeletionEffect = !value + case .contextShowReport: + SGSimpleSettings.shared.contextShowReport = value + case .contextShowReply: + SGSimpleSettings.shared.contextShowReply = value + case .contextShowPin: + SGSimpleSettings.shared.contextShowPin = value + case .contextShowSaveMedia: + SGSimpleSettings.shared.contextShowSaveMedia = value + case .contextShowMessageReplies: + SGSimpleSettings.shared.contextShowMessageReplies = value + case .stickerTimestamp: + SGSimpleSettings.shared.stickerTimestamp = value + case .contextShowJson: + SGSimpleSettings.shared.contextShowJson = value + case .hideRecordingButton: + SGSimpleSettings.shared.hideRecordingButton = !value + case .hideTabBar: + SGSimpleSettings.shared.hideTabBar = value + simplePromise.set(true) // Trigger update for 'enabled' field of other toggles + askForRestart?() + case .showDC: + SGSimpleSettings.shared.showDC = value + case .showCreationDate: + SGSimpleSettings.shared.showCreationDate = value + case .showRegDate: + SGSimpleSettings.shared.showRegDate = value + case .compactChatList: + SGSimpleSettings.shared.compactChatList = value + askForRestart?() + case .compactFolderNames: + SGSimpleSettings.shared.compactFolderNames = value + case .allChatsHidden: + SGSimpleSettings.shared.allChatsHidden = value + askForRestart?() + case .defaultEmojisFirst: + SGSimpleSettings.shared.defaultEmojisFirst = value + case .messageDoubleTapActionOutgoingEdit: + SGSimpleSettings.shared.messageDoubleTapActionOutgoing = value ? SGSimpleSettings.MessageDoubleTapAction.edit.rawValue : SGSimpleSettings.MessageDoubleTapAction.default.rawValue + case .wideChannelPosts: + SGSimpleSettings.shared.wideChannelPosts = value + case .forceEmojiTab: + SGSimpleSettings.shared.forceEmojiTab = value + case .forceBuiltInMic: + SGSimpleSettings.shared.forceBuiltInMic = value + case .hideChannelBottomButton: + SGSimpleSettings.shared.hideChannelBottomButton = value + case .confirmCalls: + SGSimpleSettings.shared.confirmCalls = value + } + }, updateSliderValue: { setting, value in + switch (setting) { + case .accountColorsSaturation: + if SGSimpleSettings.shared.accountColorsSaturation != value { + SGSimpleSettings.shared.accountColorsSaturation = value + simplePromise.set(true) + } + case .outgoingPhotoQuality: + if SGSimpleSettings.shared.outgoingPhotoQuality != value { + SGSimpleSettings.shared.outgoingPhotoQuality = value + simplePromise.set(true) + } + case .stickerSize: + if SGSimpleSettings.shared.stickerSize != value { + SGSimpleSettings.shared.stickerSize = value + simplePromise.set(true) + } + } + + }, setOneFromManyValue: { setting in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + switch (setting) { + case .downloadSpeedBoost: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.downloadSpeedBoost = value + + let enableDownloadX: Bool + switch (value) { + case SGSimpleSettings.DownloadSpeedBoostValues.none.rawValue: + enableDownloadX = false + default: + enableDownloadX = true + } + + // Updating controller + simplePromise.set(true) + + let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in + var settings = settings + settings.useExperimentalDownload = enableDownloadX + return settings + }).start(completed: { + Queue.mainQueue().async { + askForRestart?() + } + }) + } + + for value in SGSimpleSettings.DownloadSpeedBoostValues.allCases { + items.append(ActionSheetButtonItem(title: i18n("Settings.DownloadsBoost.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } + case .bottomTabStyle: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.bottomTabStyle = value + simplePromise.set(true) + } + + for value in SGSimpleSettings.BottomTabStyleValues.allCases { + items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.BottomTabStyle.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } + case .allChatsTitleLengthOverride: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.allChatsTitleLengthOverride = value + simplePromise.set(true) + } + + for value in SGSimpleSettings.AllChatsTitleLengthOverride.allCases { + let title: String + switch (value) { + case SGSimpleSettings.AllChatsTitleLengthOverride.short: + title = "\"\(presentationData.strings.ChatList_Tabs_All)\"" + case SGSimpleSettings.AllChatsTitleLengthOverride.long: + title = "\"\(presentationData.strings.ChatList_Tabs_AllChats)\"" + default: + title = i18n("Settings.Folders.AllChatsTitle.none", presentationData.strings.baseLanguageCode) + } + items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } +// case .allChatsFolderPositionOverride: +// let setAction: (String) -> Void = { value in +// SGSimpleSettings.shared.allChatsFolderPositionOverride = value +// simplePromise.set(true) +// } +// +// for value in SGSimpleSettings.AllChatsFolderPositionOverride.allCases { +// items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.AllChatsTitle.\(value)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// setAction(value.rawValue) +// })) +// } + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, openDisclosureLink: { link in + switch (link) { + case .languageSettings: + pushControllerImpl?(context.sharedContext.makeLocalizationListController(context: context)) + case .contentSettings: + let _ = (getSGSettingsURL(context: context) |> deliverOnMainQueue).start(next: { [weak context] url in + guard let strongContext = context else { + return + } + strongContext.sharedContext.applicationBindings.openUrl(url) + }) + } + }, searchInput: { searchQuery in + updateState { state in + var updatedState = state + updatedState.searchQuery = searchQuery + return updatedState + } + }) + + let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) + let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings, PreferencesKeys.appConfiguration]) + let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network) + |> map(Optional.init) + let contentSettingsConfiguration = Promise() + contentSettingsConfiguration.set(.single(nil) + |> then(updatedContentSettingsConfiguration)) + + let signal = combineLatest(simplePromise.get(), /*sliderPromise.get(),*/ statePromise.get(), context.sharedContext.presentationData, sharedData, preferences, contentSettingsConfiguration.get(), + context.engine.accountData.observeAvailableColorOptions(scope: .replies), + context.engine.accountData.observeAvailableColorOptions(scope: .profile) + ) + |> map { _, /*sliderValue,*/ state, presentationData, sharedData, view, contentSettingsConfiguration, availableReplyColors, availableProfileColors -> (ItemListControllerState, (ItemListNodeState, Any)) in + + let sgUISettings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? SGUISettings.default + let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let callListSettings: CallListSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) ?? CallListSettings.defaultSettings + let experimentalUISettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + + let entries = SGControllerEntries(presentationData: presentationData, callListSettings: callListSettings, experimentalUISettings: experimentalUISettings, SGSettings: sgUISettings, appConfiguration: appConfiguration, nameColors: PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors), state: state) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + + // TODO(swiftgram): focusOnItemTag support + /* var index = 0 + var scrollToItem: ListViewScrollToItem? + if let focusOnItemTag = focusOnItemTag { + for entry in entries { + if entry.tag?.isEqual(to: focusOnItemTag) ?? false { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) + } + index += 1 + } + } */ + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ ) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } +// getRootControllerImpl = { [weak controller] in +// return controller?.view.window?.rootViewController +// } +// getNavigationControllerImpl = { [weak controller] in +// return controller?.navigationController as? NavigationController +// } + askForRestart = { [weak context] in + guard let context = context else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?( + UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, // i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode), + text: i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode), + timeout: nil, + customUndoText: i18n("Common.RestartNow", presentationData.strings.baseLanguageCode) //presentationData.strings.Common_Yes + ), + elevatedLayout: false, + action: { action in if action == .undo { exit(0) }; return true } + ), + nil + ) + } + return controller + +} diff --git a/Swiftgram/SGShowMessageJson/BUILD b/Swiftgram/SGShowMessageJson/BUILD new file mode 100644 index 00000000000..8097e4c906a --- /dev/null +++ b/Swiftgram/SGShowMessageJson/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGShowMessageJson", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift b/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift new file mode 100644 index 00000000000..7868b0db3ad --- /dev/null +++ b/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift @@ -0,0 +1,76 @@ +import Foundation +import Wrap +import SGLogging +import ChatControllerInteraction +import ChatPresentationInterfaceState +import Postbox +import TelegramCore +import AccountContext + +public func showMessageJson(controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, message: Message, context: AccountContext) { + if let navigationController = controllerInteraction.navigationController(), let rootController = navigationController.view.window?.rootViewController { + var writingOptions: JSONSerialization.WritingOptions = [ + .prettyPrinted, + //.sortedKeys, + ] + if #available(iOS 13.0, *) { + writingOptions.insert(.withoutEscapingSlashes) + } + + var messageData: Data? = nil + do { + messageData = try wrap( + message, + writingOptions: writingOptions + ) + } catch { + SGLogger.shared.log("ShowMessageJSON", "Error parsing data: \(error)") + messageData = nil + } + + guard let messageData = messageData else { return } + + let id = Int64.random(in: Int64.min ... Int64.max) + let fileResource = LocalFileMediaResource(fileId: id, size: Int64(messageData.count), isSecretRelated: false) + context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: messageData, synchronous: true) + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/json; charset=utf-8", size: Int64(messageData.count), attributes: [.FileName(fileName: "message.json")], alternativeRepresentations: []) + + presentDocumentPreviewController(rootController: rootController, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, postbox: context.account.postbox, file: file, canShare: !message.isCopyProtected()) + + } +} + +extension MemoryBuffer: @retroactive WrapCustomizable { + + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + let hexString = self.description + return ["string": hexStringToString(hexString) ?? hexString] + } +} + +// There's a chacne we will need it for each empty/weird type, or it will be a runtime crash. +extension ContentRequiresValidationMessageAttribute: @retroactive WrapCustomizable { + + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return ["@type": "ContentRequiresValidationMessageAttribute"] + } +} + +func hexStringToString(_ hexString: String) -> String? { + var chars = Array(hexString) + var result = "" + + while chars.count > 0 { + let c = String(chars[0...1]) + chars = Array(chars.dropFirst(2)) + if let byte = UInt8(c, radix: 16) { + let scalar = UnicodeScalar(byte) + result.append(String(scalar)) + } else { + return nil + } + } + + return result +} diff --git a/Swiftgram/SGSimpleSettings/BUILD b/Swiftgram/SGSimpleSettings/BUILD new file mode 100644 index 00000000000..38462c47725 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGSimpleSettings", + module_name = "SGSimpleSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift b/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift new file mode 100644 index 00000000000..b0d073605dc --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift @@ -0,0 +1,58 @@ +//// A copy of Atomic from SwiftSignalKit +//import Foundation +// +//public enum AtomicWrapperLockError: Error { +// case isLocked +//} +// +//public final class AtomicWrapper { +// private var lock: pthread_mutex_t +// private var value: T +// +// public init(value: T) { +// self.lock = pthread_mutex_t() +// self.value = value +// +// pthread_mutex_init(&self.lock, nil) +// } +// +// deinit { +// pthread_mutex_destroy(&self.lock) +// } +// +// public func with(_ f: (T) -> R) -> R { +// pthread_mutex_lock(&self.lock) +// let result = f(self.value) +// pthread_mutex_unlock(&self.lock) +// +// return result +// } +// +// public func tryWith(_ f: (T) -> R) throws -> R { +// if pthread_mutex_trylock(&self.lock) == 0 { +// let result = f(self.value) +// pthread_mutex_unlock(&self.lock) +// return result +// } else { +// throw AtomicWrapperLockError.isLocked +// } +// } +// +// public func modify(_ f: (T) -> T) -> T { +// pthread_mutex_lock(&self.lock) +// let result = f(self.value) +// self.value = result +// pthread_mutex_unlock(&self.lock) +// +// return result +// } +// +// public func swap(_ value: T) -> T { +// pthread_mutex_lock(&self.lock) +// let previous = self.value +// self.value = value +// pthread_mutex_unlock(&self.lock) +// +// return previous +// } +//} diff --git a/Swiftgram/SGSimpleSettings/Sources/RWLock.swift b/Swiftgram/SGSimpleSettings/Sources/RWLock.swift new file mode 100644 index 00000000000..3ea2436c6f5 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/RWLock.swift @@ -0,0 +1,36 @@ +// +// RWLock.swift +// SwiftConcurrentCollections +// +// Created by Pete Prokop on 09/02/2020. +// Copyright © 2020 Pete Prokop. All rights reserved. +// + +import Foundation + +public final class RWLock { + private var lock: pthread_rwlock_t + + // MARK: Lifecycle + deinit { + pthread_rwlock_destroy(&lock) + } + + public init() { + lock = pthread_rwlock_t() + pthread_rwlock_init(&lock, nil) + } + + // MARK: Public + public func writeLock() { + pthread_rwlock_wrlock(&lock) + } + + public func readLock() { + pthread_rwlock_rdlock(&lock) + } + + public func unlock() { + pthread_rwlock_unlock(&lock) + } +} diff --git a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift new file mode 100644 index 00000000000..83e77a31cd1 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift @@ -0,0 +1,414 @@ +import Foundation + + +public class SGSimpleSettings { + + public static let shared = SGSimpleSettings() + + private init() { + setDefaultValues() + preCacheValues() + } + + private func setDefaultValues() { + UserDefaults.standard.register(defaults: SGSimpleSettings.defaultValues) + } + + private func preCacheValues() { + // let dispatchGroup = DispatchGroup() + + let tasks = [ +// { let _ = self.allChatsFolderPositionOverride }, + { let _ = self.allChatsHidden }, + { let _ = self.hideTabBar }, + { let _ = self.bottomTabStyle }, + { let _ = self.compactChatList }, + { let _ = self.compactFolderNames }, + { let _ = self.disableSwipeToRecordStory }, + { let _ = self.rememberLastFolder }, + { let _ = self.quickTranslateButton }, + { let _ = self.stickerSize }, + { let _ = self.stickerTimestamp }, + { let _ = self.hideReactions }, + { let _ = self.disableGalleryCamera }, + { let _ = self.disableSendAsButton }, + { let _ = self.disableSnapDeletionEffect }, + { let _ = self.startTelescopeWithRearCam }, + { let _ = self.hideRecordingButton } + ] + + tasks.forEach { task in + DispatchQueue.global(qos: .background).async(/*group: dispatchGroup*/) { + task() + } + } + + // dispatchGroup.notify(queue: DispatchQueue.main) {} + } + + public enum Keys: String, CaseIterable { + case hidePhoneInSettings + case showTabNames + case startTelescopeWithRearCam + case accountColorsSaturation + case uploadSpeedBoost + case downloadSpeedBoost + case bottomTabStyle + case rememberLastFolder + case lastAccountFolders + case localDNSForProxyHost + case sendLargePhotos + case outgoingPhotoQuality + case storyStealthMode + case canUseStealthMode + case disableSwipeToRecordStory + case quickTranslateButton + case outgoingLanguageTranslation + case hideReactions + case showRepostToStory + case contextShowSelectFromUser + case contextShowSaveToCloud + case contextShowRestrict + // case contextShowBan + case contextShowHideForwardName + case contextShowReport + case contextShowReply + case contextShowPin + case contextShowSaveMedia + case contextShowMessageReplies + case contextShowJson + case disableScrollToNextChannel + case disableScrollToNextTopic + case disableChatSwipeOptions + case disableDeleteChatSwipeOption + case disableGalleryCamera + case disableSendAsButton + case disableSnapDeletionEffect + case stickerSize + case stickerTimestamp + case hideRecordingButton + case hideTabBar + case showDC + case showCreationDate + case showRegDate + case regDateCache + case compactChatList + case compactFolderNames + case allChatsTitleLengthOverride +// case allChatsFolderPositionOverride + case allChatsHidden + case defaultEmojisFirst + case messageDoubleTapActionOutgoing + case wideChannelPosts + case forceEmojiTab + case forceBuiltInMic + case hideChannelBottomButton + case forceSystemSharing + case confirmCalls + } + + public enum DownloadSpeedBoostValues: String, CaseIterable { + case none + case medium + case maximum + } + + public enum BottomTabStyleValues: String, CaseIterable { + case telegram + case ios + } + + public enum AllChatsTitleLengthOverride: String, CaseIterable { + case none + case short + case long + } + + public enum AllChatsFolderPositionOverride: String, CaseIterable { + case none + case last + case hidden + } + + public enum MessageDoubleTapAction: String, CaseIterable { + case `default` + case none + case edit + } + + public static let defaultValues: [String: Any] = [ + Keys.hidePhoneInSettings.rawValue: true, + Keys.showTabNames.rawValue: true, + Keys.startTelescopeWithRearCam.rawValue: false, + Keys.accountColorsSaturation.rawValue: 100, + Keys.uploadSpeedBoost.rawValue: false, + Keys.downloadSpeedBoost.rawValue: DownloadSpeedBoostValues.none.rawValue, + Keys.rememberLastFolder.rawValue: false, + Keys.bottomTabStyle.rawValue: BottomTabStyleValues.telegram.rawValue, + Keys.lastAccountFolders.rawValue: [:], + Keys.localDNSForProxyHost.rawValue: false, + Keys.sendLargePhotos.rawValue: false, + Keys.outgoingPhotoQuality.rawValue: 70, + Keys.storyStealthMode.rawValue: false, + Keys.canUseStealthMode.rawValue: true, + Keys.disableSwipeToRecordStory.rawValue: false, + Keys.quickTranslateButton.rawValue: false, + Keys.outgoingLanguageTranslation.rawValue: [:], + Keys.hideReactions.rawValue: false, + Keys.showRepostToStory.rawValue: true, + Keys.contextShowSelectFromUser.rawValue: true, + Keys.contextShowSaveToCloud.rawValue: true, + Keys.contextShowRestrict.rawValue: true, + // Keys.contextShowBan.rawValue: true, + Keys.contextShowHideForwardName.rawValue: true, + Keys.contextShowReport.rawValue: true, + Keys.contextShowReply.rawValue: true, + Keys.contextShowPin.rawValue: true, + Keys.contextShowSaveMedia.rawValue: true, + Keys.contextShowMessageReplies.rawValue: true, + Keys.contextShowJson.rawValue: false, + Keys.disableScrollToNextChannel.rawValue: false, + Keys.disableScrollToNextTopic.rawValue: false, + Keys.disableChatSwipeOptions.rawValue: false, + Keys.disableDeleteChatSwipeOption.rawValue: false, + Keys.disableGalleryCamera.rawValue: false, + Keys.disableSendAsButton.rawValue: false, + Keys.disableSnapDeletionEffect.rawValue: false, + Keys.stickerSize.rawValue: 100, + Keys.stickerTimestamp.rawValue: true, + Keys.hideRecordingButton.rawValue: false, + Keys.hideTabBar.rawValue: false, + Keys.showDC.rawValue: false, + Keys.showCreationDate.rawValue: true, + Keys.showRegDate.rawValue: true, + Keys.regDateCache.rawValue: [:], + Keys.compactChatList.rawValue: false, + Keys.compactFolderNames.rawValue: false, + Keys.allChatsTitleLengthOverride.rawValue: AllChatsTitleLengthOverride.none.rawValue, +// Keys.allChatsFolderPositionOverride.rawValue: AllChatsFolderPositionOverride.none.rawValue + Keys.allChatsHidden.rawValue: false, + Keys.defaultEmojisFirst.rawValue: false, + Keys.messageDoubleTapActionOutgoing.rawValue: MessageDoubleTapAction.default.rawValue, + Keys.wideChannelPosts.rawValue: false, + Keys.forceEmojiTab.rawValue: false, + Keys.hideChannelBottomButton.rawValue: false, + Keys.forceSystemSharing.rawValue: false, + Keys.confirmCalls.rawValue: true, + ] + + @UserDefault(key: Keys.hidePhoneInSettings.rawValue) + public var hidePhoneInSettings: Bool + + @UserDefault(key: Keys.showTabNames.rawValue) + public var showTabNames: Bool + + @UserDefault(key: Keys.startTelescopeWithRearCam.rawValue) + public var startTelescopeWithRearCam: Bool + + @UserDefault(key: Keys.accountColorsSaturation.rawValue) + public var accountColorsSaturation: Int32 + + @UserDefault(key: Keys.uploadSpeedBoost.rawValue) + public var uploadSpeedBoost: Bool + + @UserDefault(key: Keys.downloadSpeedBoost.rawValue) + public var downloadSpeedBoost: String + + @UserDefault(key: Keys.rememberLastFolder.rawValue) + public var rememberLastFolder: Bool + + @UserDefault(key: Keys.bottomTabStyle.rawValue) + public var bottomTabStyle: String + + public var lastAccountFolders = UserDefaultsBackedDictionary(userDefaultsKey: Keys.lastAccountFolders.rawValue, threadSafe: false) + + @UserDefault(key: Keys.localDNSForProxyHost.rawValue) + public var localDNSForProxyHost: Bool + + @UserDefault(key: Keys.sendLargePhotos.rawValue) + public var sendLargePhotos: Bool + + @UserDefault(key: Keys.outgoingPhotoQuality.rawValue) + public var outgoingPhotoQuality: Int32 + + @UserDefault(key: Keys.storyStealthMode.rawValue) + public var storyStealthMode: Bool + + @UserDefault(key: Keys.canUseStealthMode.rawValue) + public var canUseStealthMode: Bool + + @UserDefault(key: Keys.disableSwipeToRecordStory.rawValue) + public var disableSwipeToRecordStory: Bool + + @UserDefault(key: Keys.quickTranslateButton.rawValue) + public var quickTranslateButton: Bool + + public var outgoingLanguageTranslation = UserDefaultsBackedDictionary(userDefaultsKey: Keys.outgoingLanguageTranslation.rawValue, threadSafe: false) + + @UserDefault(key: Keys.hideReactions.rawValue) + public var hideReactions: Bool + + @UserDefault(key: Keys.showRepostToStory.rawValue) + public var showRepostToStory: Bool + + @UserDefault(key: Keys.contextShowRestrict.rawValue) + public var contextShowRestrict: Bool + + /*@UserDefault(key: Keys.contextShowBan.rawValue) + public var contextShowBan: Bool*/ + + @UserDefault(key: Keys.contextShowSelectFromUser.rawValue) + public var contextShowSelectFromUser: Bool + + @UserDefault(key: Keys.contextShowSaveToCloud.rawValue) + public var contextShowSaveToCloud: Bool + + @UserDefault(key: Keys.contextShowHideForwardName.rawValue) + public var contextShowHideForwardName: Bool + + @UserDefault(key: Keys.contextShowReport.rawValue) + public var contextShowReport: Bool + + @UserDefault(key: Keys.contextShowReply.rawValue) + public var contextShowReply: Bool + + @UserDefault(key: Keys.contextShowPin.rawValue) + public var contextShowPin: Bool + + @UserDefault(key: Keys.contextShowSaveMedia.rawValue) + public var contextShowSaveMedia: Bool + + @UserDefault(key: Keys.contextShowMessageReplies.rawValue) + public var contextShowMessageReplies: Bool + + @UserDefault(key: Keys.contextShowJson.rawValue) + public var contextShowJson: Bool + + @UserDefault(key: Keys.disableScrollToNextChannel.rawValue) + public var disableScrollToNextChannel: Bool + + @UserDefault(key: Keys.disableScrollToNextTopic.rawValue) + public var disableScrollToNextTopic: Bool + + @UserDefault(key: Keys.disableChatSwipeOptions.rawValue) + public var disableChatSwipeOptions: Bool + + @UserDefault(key: Keys.disableDeleteChatSwipeOption.rawValue) + public var disableDeleteChatSwipeOption: Bool + + @UserDefault(key: Keys.disableGalleryCamera.rawValue) + public var disableGalleryCamera: Bool + + @UserDefault(key: Keys.disableSendAsButton.rawValue) + public var disableSendAsButton: Bool + + @UserDefault(key: Keys.disableSnapDeletionEffect.rawValue) + public var disableSnapDeletionEffect: Bool + + @UserDefault(key: Keys.stickerSize.rawValue) + public var stickerSize: Int32 + + @UserDefault(key: Keys.stickerTimestamp.rawValue) + public var stickerTimestamp: Bool + + @UserDefault(key: Keys.hideRecordingButton.rawValue) + public var hideRecordingButton: Bool + + @UserDefault(key: Keys.hideTabBar.rawValue) + public var hideTabBar: Bool + + @UserDefault(key: Keys.showDC.rawValue) + public var showDC: Bool + + @UserDefault(key: Keys.showCreationDate.rawValue) + public var showCreationDate: Bool + + @UserDefault(key: Keys.showRegDate.rawValue) + public var showRegDate: Bool + + public var regDateCache = UserDefaultsBackedDictionary(userDefaultsKey: Keys.regDateCache.rawValue, threadSafe: false) + + @UserDefault(key: Keys.compactChatList.rawValue) + public var compactChatList: Bool + + @UserDefault(key: Keys.compactFolderNames.rawValue) + public var compactFolderNames: Bool + + @UserDefault(key: Keys.allChatsTitleLengthOverride.rawValue) + public var allChatsTitleLengthOverride: String +// +// @UserDefault(key: Keys.allChatsFolderPositionOverride.rawValue) +// public var allChatsFolderPositionOverride: String + @UserDefault(key: Keys.allChatsHidden.rawValue) + public var allChatsHidden: Bool + + @UserDefault(key: Keys.defaultEmojisFirst.rawValue) + public var defaultEmojisFirst: Bool + + @UserDefault(key: Keys.messageDoubleTapActionOutgoing.rawValue) + public var messageDoubleTapActionOutgoing: String + + @UserDefault(key: Keys.wideChannelPosts.rawValue) + public var wideChannelPosts: Bool + + @UserDefault(key: Keys.forceEmojiTab.rawValue) + public var forceEmojiTab: Bool + + @UserDefault(key: Keys.forceBuiltInMic.rawValue) + public var forceBuiltInMic: Bool + + @UserDefault(key: Keys.hideChannelBottomButton.rawValue) + public var hideChannelBottomButton: Bool + + @UserDefault(key: Keys.forceSystemSharing.rawValue) + public var forceSystemSharing: Bool + + @UserDefault(key: Keys.confirmCalls.rawValue) + public var confirmCalls: Bool +} + +extension SGSimpleSettings { + public var isStealthModeEnabled: Bool { + return storyStealthMode && canUseStealthMode + } + + public static func makeOutgoingLanguageTranslationKey(accountId: Int64, peerId: Int64) -> String { + return "\(accountId):\(peerId)" + } +} + +public func getSGDownloadPartSize(_ default: Int64) -> Int64 { + let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost + switch (currentDownloadSetting) { + case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue: + return 512 * 1024 + case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue: + return 1024 * 1024 + default: + return `default` + } +} + +public func getSGMaxPendingParts(_ default: Int) -> Int { + let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost + switch (currentDownloadSetting) { + case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue: + return 8 + case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue: + return 12 + default: + return `default` + } +} + +public func sgUseShortAllChatsTitle(_ default: Bool) -> Bool { + let currentOverride = SGSimpleSettings.shared.allChatsTitleLengthOverride + switch (currentOverride) { + case SGSimpleSettings.AllChatsTitleLengthOverride.short.rawValue: + return true + case SGSimpleSettings.AllChatsTitleLengthOverride.long.rawValue: + return false + default: + return `default` + } +} diff --git a/Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift b/Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift new file mode 100644 index 00000000000..48b0a377494 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift @@ -0,0 +1,406 @@ +import Foundation + +public protocol AllowedUserDefaultTypes {} + +/* // This one is more painful than helpful +extension Bool: AllowedUserDefaultTypes {} +extension String: AllowedUserDefaultTypes {} +extension Int: AllowedUserDefaultTypes {} +extension Int32: AllowedUserDefaultTypes {} +extension Double: AllowedUserDefaultTypes {} +extension Float: AllowedUserDefaultTypes {} +extension Data: AllowedUserDefaultTypes {} +extension URL: AllowedUserDefaultTypes {} +//extension Dictionary: AllowedUserDefaultTypes {} +extension Array: AllowedUserDefaultTypes where Element: AllowedUserDefaultTypes {} +*/ + +// Does not support Optional types due to caching +@propertyWrapper +public class UserDefault /*where T: AllowedUserDefaultTypes*/ { + public let key: String + public let userDefaults: UserDefaults + private var cachedValue: T? + + public init(key: String, userDefaults: UserDefaults = .standard) { + self.key = key + self.userDefaults = userDefaults + } + + public var wrappedValue: T { + get { + #if DEBUG && false + SGtrace("UD.\(key)", what: "GET") + #endif + + if let strongCachedValue = cachedValue { + #if DEBUG && false + SGtrace("UD", what: "CACHED \(key) \(strongCachedValue)") + #endif + return strongCachedValue + } + + cachedValue = readFromUserDefaults() + + #if DEBUG + SGtrace("UD.\(key)", what: "EXTRACTED: \(cachedValue!)") + #endif + return cachedValue! + } + set { + cachedValue = newValue + #if DEBUG + SGtrace("UD.\(key)", what: "CACHE UPDATED \(cachedValue!)") + #endif + userDefaults.set(newValue, forKey: key) + } + } + + fileprivate func readFromUserDefaults() -> T { + switch T.self { + case is Bool.Type: + return (userDefaults.bool(forKey: key) as! T) + case is String.Type: + return (userDefaults.string(forKey: key) as! T) + case is Int32.Type: + return (Int32(exactly: userDefaults.integer(forKey: key)) as! T) + case is Int.Type: + return (userDefaults.integer(forKey: key) as! T) + case is Double.Type: + return (userDefaults.double(forKey: key) as! T) + case is Float.Type: + return (userDefaults.float(forKey: key) as! T) + case is Data.Type: + return (userDefaults.data(forKey: key) as! T) + case is URL.Type: + return (userDefaults.url(forKey: key) as! T) + case is Array.Type: + return (userDefaults.stringArray(forKey: key) as! T) + case is Array.Type: + return (userDefaults.array(forKey: key) as! T) + default: + fatalError("Unsupported UserDefault type \(T.self)") + // cachedValue = (userDefaults.object(forKey: key) as! T) + } + } +} + +//public class AtomicUserDefault: UserDefault { +// private let atomicCachedValue: AtomicWrapper = AtomicWrapper(value: nil) +// +// public override var wrappedValue: T { +// get { +// return atomicCachedValue.modify({ value in +// if let strongValue = value { +// return strongValue +// } +// return self.readFromUserDefaults() +// })! +// } +// set { +// let _ = atomicCachedValue.modify({ _ in +// userDefaults.set(newValue, forKey: key) +// return newValue +// }) +// } +// } +//} + + + +// Based on ConcurrentDictionary.swift from https://github.com/peterprokop/SwiftConcurrentCollections + +/// Thread-safe UserDefaults dictionary wrapper +/// - Important: Note that this is a `class`, i.e. reference (not value) type +/// - Important: Key can only be String type +public class UserDefaultsBackedDictionary { + public let userDefaultsKey: String + public let userDefaults: UserDefaults + + private var container: [Key: Value]? = nil + private let rwlock = RWLock() + private let threadSafe: Bool + + public var keys: [Key] { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "KEYS") + #endif + let result: [Key] + if threadSafe { + rwlock.readLock() + } + if container == nil { + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)") + #endif + } else { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)") + #endif + } + result = Array(container!.keys) + if threadSafe { + rwlock.unlock() + } + return result + } + + public var values: [Value] { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUES") + #endif + let result: [Value] + if threadSafe { + rwlock.readLock() + } + if container == nil { + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)") + #endif + } else { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)") + #endif + } + result = Array(container!.values) + if threadSafe { + rwlock.unlock() + } + return result + } + + public init(userDefaultsKey: String, userDefaults: UserDefaults = .standard, threadSafe: Bool) { + self.userDefaultsKey = userDefaultsKey + self.userDefaults = userDefaults + self.threadSafe = threadSafe + } + + /// Sets the value for key + /// + /// - Parameters: + /// - value: The value to set for key + /// - key: The key to set value for + public func set(value: Value, forKey key: Key) { + if threadSafe { + rwlock.writeLock() + } + _set(value: value, forKey: key) + if threadSafe { + rwlock.unlock() + } + } + + @discardableResult + public func remove(_ key: Key) -> Value? { + let result: Value? + if threadSafe { + rwlock.writeLock() + } + result = _remove(key) + if threadSafe { + rwlock.unlock() + } + return result + } + + @discardableResult + public func removeValue(forKey: Key) -> Value? { + return self.remove(forKey) + } + + public func contains(_ key: Key) -> Bool { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CONTAINS") + #endif + let result: Bool + if threadSafe { + rwlock.readLock() + } + if container == nil { + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)") + #endif + } else { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)") + #endif + } + result = container!.index(forKey: key) != nil + if threadSafe { + rwlock.unlock() + } + return result + } + + public func value(forKey key: Key) -> Value? { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUE") + #endif + let result: Value? + if threadSafe { + rwlock.readLock() + } + if container == nil { + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)") + #endif + } else { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)") + #endif + } + result = container![key] + if threadSafe { + rwlock.unlock() + } + return result + } + + public func mutateValue(forKey key: Key, mutation: (Value) -> Value) { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "MUTATE") + #endif + if threadSafe { + rwlock.writeLock() + } + if container == nil { + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)") + #endif + } else { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)") + #endif + } + if let value = container![key] { + container![key] = mutation(value) + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)") + #endif + userDefaultsContainer = container! + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)") + #endif + } + if threadSafe { + rwlock.unlock() + } + } + + public var isEmpty: Bool { + return self.keys.isEmpty + } + + // MARK: Subscript + public subscript(key: Key) -> Value? { + get { + return value(forKey: key) + } + set { + if threadSafe { + rwlock.writeLock() + } + defer { + if threadSafe { + rwlock.unlock() + } + } + guard let newValue = newValue else { + _remove(key) + return + } + _set(value: newValue, forKey: key) + } + } + + // MARK: Private + @inline(__always) + private func _set(value: Value, forKey key: Key) { + if container == nil { + container = userDefaultsContainer + } + self.container![key] = value + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)") + #endif + userDefaultsContainer = container! + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)") + #endif + } + + @inline(__always) + @discardableResult + private func _remove(_ key: Key) -> Value? { + if container == nil { + container = userDefaultsContainer + } + guard let index = container!.index(forKey: key) else { return nil } + + let tuple = container!.remove(at: index) + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE REMOVE \(key) \(container!)") + #endif + userDefaultsContainer = container! + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED REMOVED \(key) \(container!)") + #endif + return tuple.value + } + + private var userDefaultsContainer: [Key: Value] { + get { + return userDefaults.dictionary(forKey: userDefaultsKey) as! [Key: Value] + } + set { + userDefaults.set(newValue, forKey: userDefaultsKey) + } + } + + public func drop() { + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPING") + #endif + if threadSafe { + rwlock.writeLock() + } + userDefaults.removeObject(forKey: userDefaultsKey) + container = userDefaultsContainer + #if DEBUG + SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPED: \(container!)") + #endif + if threadSafe { + rwlock.unlock() + } + } + +} + + +#if DEBUG +private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility) + +public func SGtrace(_ domain: String, what: @autoclosure() -> String) { + let string = what() + var rawTime = time_t() + time(&rawTime) + var timeinfo = tm() + localtime_r(&rawTime, &timeinfo) + + var curTime = timeval() + gettimeofday(&curTime, nil) + let seconds = Int(curTime.tv_sec % 60) // Extracting the current second + let microseconds = curTime.tv_usec // Full microsecond precision + + queue.async { + let result = String(format: "[%@] %d-%d-%d %02d:%02d:%02d.%06d %@", arguments: [domain, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), seconds, microseconds, string]) + + print(result) + } +} +#endif diff --git a/Swiftgram/SGStrings/BUILD b/Swiftgram/SGStrings/BUILD new file mode 100644 index 00000000000..dea968818af --- /dev/null +++ b/Swiftgram/SGStrings/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGStrings", + module_name = "SGStrings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AppBundle:AppBundle", + "//Swiftgram/SGLogging:SGLogging" + ], + visibility = [ + "//visibility:public", + ], +) + +filegroup( + name = "SGLocalizableStrings", + srcs = glob(["Strings/*.lproj/SGLocalizable.strings"]), + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGStrings/Sources/LocalizationManager.swift b/Swiftgram/SGStrings/Sources/LocalizationManager.swift new file mode 100644 index 00000000000..51c646ceb30 --- /dev/null +++ b/Swiftgram/SGStrings/Sources/LocalizationManager.swift @@ -0,0 +1,121 @@ +import Foundation + +// Assuming NGLogging and AppBundle are custom modules, they are imported here. +import SGLogging +import AppBundle + + +public let SGFallbackLocale = "en" + +public class SGLocalizationManager { + + public static let shared = SGLocalizationManager() + + private let appBundle: Bundle + private var localizations: [String: [String: String]] = [:] + private var webLocalizations: [String: [String: String]] = [:] + private let fallbackMappings: [String: String] = [ + // "from": "to" + "zh-hant": "zh-hans", + "be": "ru", + "nb": "no", + "ckb": "ku", + "sdh": "ku" + ] + + private init(fetchLocale: String = SGFallbackLocale) { + self.appBundle = getAppBundle() + // Iterating over all the app languages and loading SGLocalizable.strings + self.appBundle.localizations.forEach { locale in + if locale != "Base" { + localizations[locale] = loadLocalDictionary(for: locale) + } + } + // Downloading one specific locale + self.downloadLocale(fetchLocale) + } + + public func localizedString(_ key: String, _ locale: String = SGFallbackLocale, args: CVarArg...) -> String { + let sanitizedLocale = self.sanitizeLocale(locale) + + if let localizedString = findLocalizedString(forKey: key, inLocale: sanitizedLocale) { + if args.isEmpty { + return String(format: localizedString) + } else { + return String(format: localizedString, arguments: args) + } + } + + SGLogger.shared.log("Strings", "Missing string for key: \(key) in locale: \(locale)") + return key + } + + private func loadLocalDictionary(for locale: String) -> [String: String] { + guard let path = self.appBundle.path(forResource: "SGLocalizable", ofType: "strings", inDirectory: nil, forLocalization: locale) else { + // SGLogger.shared.log("Localization", "Unable to find path for locale: \(locale)") + return [:] + } + + guard let dictionary = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] else { + // SGLogger.shared.log("Localization", "Unable to load dictionary for locale: \(locale)") + return [:] + } + + return dictionary + } + + public func downloadLocale(_ locale: String) { + let sanitizedLocale = self.sanitizeLocale(locale) + guard let url = URL(string: self.getStringsUrl(for: sanitizedLocale)) else { + SGLogger.shared.log("Strings", "Invalid URL for locale: \(sanitizedLocale)") + return + } + + DispatchQueue.global(qos: .background).async { + if let localeDict = NSDictionary(contentsOf: url) as? [String: String] { + DispatchQueue.main.async { + self.webLocalizations[sanitizedLocale] = localeDict + SGLogger.shared.log("Strings", "Successfully downloaded locale \(sanitizedLocale)") + } + } else { + SGLogger.shared.log("Strings", "Failed to download \(sanitizedLocale)") + } + } + } + + private func sanitizeLocale(_ locale: String) -> String { + var sanitizedLocale = locale + let rawSuffix = "-raw" + if locale.hasSuffix(rawSuffix) { + sanitizedLocale = String(locale.dropLast(rawSuffix.count)) + } + + if sanitizedLocale == "pt-br" { + sanitizedLocale = "pt" + } else if sanitizedLocale == "nb" { + sanitizedLocale = "no" + } + + return sanitizedLocale + } + + private func findLocalizedString(forKey key: String, inLocale locale: String) -> String? { + if let string = self.webLocalizations[locale]?[key], !string.isEmpty { + return string + } + if let string = self.localizations[locale]?[key], !string.isEmpty { + return string + } + if let fallbackLocale = self.fallbackMappings[locale] { + return self.findLocalizedString(forKey: key, inLocale: fallbackLocale) + } + return self.localizations[SGFallbackLocale]?[key] + } + + private func getStringsUrl(for locale: String) -> String { + return "https://raw.githubusercontent.com/Swiftgram/Telegram-iOS/master/Swiftgram/SGStrings/Strings/\(locale).lproj/SGLocalizable.strings" + } + +} + +public let i18n = SGLocalizationManager.shared.localizedString diff --git a/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..02d0e20b304 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Inhoudinstellings"; + +"Settings.Tabs.Header" = "OORTJIES"; +"Settings.Tabs.HideTabBar" = "Versteek Tabbalk"; +"Settings.Tabs.ShowContacts" = "Wys Kontak Oortjie"; +"Settings.Tabs.ShowNames" = "Wys oortjiename"; + +"Settings.Folders.BottomTab" = "Lêers onderaan"; +"Settings.Folders.BottomTabStyle" = "Bodem Lêerstyl"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Versteek \"%@\""; +"Settings.Folders.RememberLast" = "Maak laaste lêer oop"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram sal die laaste gebruikte lêer oopmaak na herbegin of rekeningwissel."; + +"Settings.Folders.CompactNames" = "Kleiner spasie"; +"Settings.Folders.AllChatsTitle" = "\"Alle Chats\" titel"; +"Settings.Folders.AllChatsTitle.short" = "Kort"; +"Settings.Folders.AllChatsTitle.long" = "Lank"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Verstek"; + + +"Settings.ChatList.Header" = "CHATLYS"; +"Settings.CompactChatList" = "Kompakte Chatlys"; + +"Settings.Profiles.Header" = "PROFIELE"; + +"Settings.Stories.Hide" = "Versteek Stories"; +"Settings.Stories.WarnBeforeView" = "Vra voor besigtiging"; +"Settings.Stories.DisableSwipeToRecord" = "Deaktiveer swiep om op te neem"; + +"Settings.Translation.QuickTranslateButton" = "Vinnige Vertaalknoppie"; + +"Stories.Warning.Author" = "Outeur"; +"Stories.Warning.ViewStory" = "Besigtig Storie?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SAL KAN SIEN dat jy hul Storie besigtig het."; +"Stories.Warning.NoticeStealth" = "%@ Sal nie kan sien dat jy hul Storie besigtig het nie."; + +"Settings.Photo.Quality.Notice" = "Kwaliteit van uitgaande foto's en fotostories."; +"Settings.Photo.SendLarge" = "Stuur groot foto's"; +"Settings.Photo.SendLarge.Notice" = "Verhoog die sybeperking op saamgeperste beelde tot 2560px."; + +"Settings.VideoNotes.Header" = "RONDE VIDEOS"; +"Settings.VideoNotes.StartWithRearCam" = "Begin met agterkamera"; + +"Settings.CustomColors.Header" = "REKENING KLEURE"; +"Settings.CustomColors.Saturation" = "VERSATIGING"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Stel versadiging op 0%% om rekening kleure te deaktiveer."; + +"Settings.UploadsBoost" = "Oplaai versterking"; +"Settings.DownloadsBoost" = "Aflaai versterking"; +"Settings.DownloadsBoost.none" = "Gedeaktiveer"; +"Settings.DownloadsBoost.medium" = "Medium"; +"Settings.DownloadsBoost.maximum" = "Maksimum"; + +"Settings.ShowProfileID" = "Wys profiel ID"; +"Settings.ShowDC" = "Wys Data Sentrum"; +"Settings.ShowCreationDate" = "Wys Geskep Datum van Geselskap"; +"Settings.ShowCreationDate.Notice" = "Die skeppingsdatum mag onbekend wees vir sommige gesprekke."; + +"Settings.ShowRegDate" = "Wys Registrasie Datum"; +"Settings.ShowRegDate.Notice" = "Die registrasiedatum is benaderend."; + +"Settings.SendWithReturnKey" = "Stuur met \"terug\" sleutel"; +"Settings.HidePhoneInSettingsUI" = "Versteek telefoon in instellings"; +"Settings.HidePhoneInSettingsUI.Notice" = "Dit sal slegs jou telefoonnommer versteek vanaf die instellingskoppelvlak. Om dit vir ander te versteek, gaan na Privaatheid en Sekuriteit."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "As weg vir 5 sekondes"; + +"ProxySettings.UseSystemDNS" = "Gebruik stelsel DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Gebruik stelsel DNS om uitvaltyd te omseil as jy nie toegang tot Google DNS het nie"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Jy **het nie nodig** %@ nie!"; +"Common.RestartRequired" = "Herbegin benodig"; +"Common.RestartNow" = "Herbegin Nou"; +"Common.OpenTelegram" = "Maak Telegram oop"; +"Common.UseTelegramForPremium" = "Let daarop dat om Telegram Premium te kry, moet jy die amptelike Telegram-app gebruik. Nadat jy Telegram Premium verkry het, sal al sy funksies beskikbaar word in Swiftgram."; + +"Message.HoldToShowOrReport" = "Hou vas om te Wys of te Rapporteer."; + +"Auth.AccountBackupReminder" = "Maak seker jy het 'n rugsteun toegangsmetode. Hou 'n SIM vir SMS of 'n addisionele sessie aangemeld om te verhoed dat jy uitgesluit word."; +"Auth.UnofficialAppCodeTitle" = "Jy kan die kode slegs met die amptelike app kry"; + +"Settings.SmallReactions" = "Klein reaksies"; +"Settings.HideReactions" = "Verberg Reaksies"; + +"ContextMenu.SaveToCloud" = "Stoor na Wolk"; +"ContextMenu.SelectFromUser" = "Kies vanaf Outeur"; + +"Settings.ContextMenu" = "KONTEKSMENU"; +"Settings.ContextMenu.Notice" = "Gedeaktiveerde inskrywings sal beskikbaar wees in die \"Swiftgram\" sub-menu."; + + +"Settings.ChatSwipeOptions" = "Chat List Swipe Options"; +"Settings.DeleteChatSwipeOption" = "Veeg om Klets Te Verwyder"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Trek na Volgende Ongelese Kanaal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Trek na Volgende Onderwerp"; +"Settings.GalleryCamera" = "Camera in Gallery"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Button"; +"Settings.SnapDeletionEffect" = "Message Deletion Effects"; + +"Settings.Stickers.Size" = "SIZE"; +"Settings.Stickers.Timestamp" = "Show Timestamp"; + +"Settings.RecordingButton" = "Voice Recording Button"; + +"Settings.DefaultEmojisFirst" = "Prioritise standaard emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Wys standaard emojis voor premium op die emoji sleutelbord"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "geskep: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Sluit aan by %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Geregistreer"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dubbelklik om boodskap te wysig"; + +"Settings.wideChannelPosts" = "Wye pos in kanale"; +"Settings.ForceEmojiTab" = "Emoji klawerbord standaard"; + +"Settings.forceBuiltInMic" = "Kragtoestel Mikrofoon"; +"Settings.forceBuiltInMic.Notice" = "Indien geaktiveer, sal die app slegs die toestel se mikrofoon gebruik selfs as oorfone aangesluit is."; + +"Settings.hideChannelBottomButton" = "Verberg Kanaal Onderpaneel"; diff --git a/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..d5143aa36fd --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "إعدادات المحتوى"; + +"Settings.Tabs.Header" = "تبويبات"; +"Settings.Tabs.HideTabBar" = "إخفاء شريط علامات التبويب"; +"Settings.Tabs.ShowContacts" = "إظهار تبويب جهات الاتصال"; +"Settings.Tabs.ShowNames" = "إظهار أسماء التبويبات"; + +"Settings.Folders.BottomTab" = "المجلدات في الأسفل"; +"Settings.Folders.BottomTabStyle" = "نمط المجلدات السفلية"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "إخفاء \"%@\""; +"Settings.Folders.RememberLast" = "فتح المجلد الأخير"; +"Settings.Folders.RememberLast.Notice" = "سيفتح Swiftgram آخر مجلد مستخدم عند إعادة تشغيل التطبيق أو تبديل الحسابات."; + +"Settings.Folders.CompactNames" = "مسافات أصغر"; +"Settings.Folders.AllChatsTitle" = "عنوان \"كل المحادثات\""; +"Settings.Folders.AllChatsTitle.short" = "قصير"; +"Settings.Folders.AllChatsTitle.long" = "طويل"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "الافتراضي"; + + +"Settings.ChatList.Header" = "قائمة الفواصل"; +"Settings.CompactChatList" = "قائمة الدردشة المتراصة"; + +"Settings.Profiles.Header" = "الملفات الشخصية"; + +"Settings.Stories.Hide" = "إخفاء القصص"; +"Settings.Stories.WarnBeforeView" = "اسأل قبل العرض"; +"Settings.Stories.DisableSwipeToRecord" = "تعطيل السحب للتسجيل"; + +"Settings.Translation.QuickTranslateButton" = "زر الترجمة الفوري"; + +"Stories.Warning.Author" = "الكاتب"; +"Stories.Warning.ViewStory" = "عرض القصة؟"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ WILL BE يتم إخبارهم بأنك شاهدت قصتهم."; +"Stories.Warning.NoticeStealth" = "%@ لن يتمكن من رؤية أنك شاهدت قصته."; + +"Settings.Photo.Quality.Notice" = "جودة الصور والصور الصادرة والقصص."; +"Settings.Photo.SendLarge" = "إرسال صور كبيرة"; +"Settings.Photo.SendLarge.Notice" = "زيادة الحد الجانبي للصور المضغوطة إلى 2560 بكسل."; + +"Settings.VideoNotes.Header" = "فيديوهات مستديرة"; +"Settings.VideoNotes.StartWithRearCam" = "البدء بالكاميرا الخلفية"; + +"Settings.CustomColors.Header" = "ألوان الحساب"; +"Settings.CustomColors.Saturation" = "مستوى التشبع"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "تعيين التشبع إلى 0%% لتعطيل ألوان الحساب."; + +"Settings.UploadsBoost" = "تعزيز التحميلات"; +"Settings.DownloadsBoost" = "تعزيز التنزيلات"; +"Settings.DownloadsBoost.none" = "تعطيل"; +"Settings.DownloadsBoost.medium" = "متوسط"; +"Settings.DownloadsBoost.maximum" = "الحد الاقصى"; + +"Settings.ShowProfileID" = "إظهار معرف الملف الشخصي ID"; +"Settings.ShowDC" = "إظهار مركز البيانات"; +"Settings.ShowCreationDate" = "إظهار تاريخ إنشاء المحادثة"; +"Settings.ShowCreationDate.Notice" = "قد يكون تاريخ الإنشاء مفقوداً لبضع المحادثات."; + +"Settings.ShowRegDate" = "إظهار تاريخ التسجيل"; +"Settings.ShowRegDate.Notice" = "تاريخ التسجيل تقريبي."; + +"Settings.SendWithReturnKey" = "إرسال مع مفتاح \"العودة\""; +"Settings.HidePhoneInSettingsUI" = "إخفاء الرقم من الإعدادات"; +"Settings.HidePhoneInSettingsUI.Notice" = "سيتم اخفاء رقمك من التطبيق فقط. لأخفاءهُ من المستخدمين الآخرين، يرجى استخدام إعدادات الخصوصية."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "إذا كان بعيدا لمدة 5 ثوان"; + +"ProxySettings.UseSystemDNS" = "استخدم DNS النظام"; +"ProxySettings.UseSystemDNS.Notice" = "استخدم نظام DNS لتجاوز المهلة إذا لم تكن لديك حق الوصول إلى Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "أنت **لا تحتاج** %@!"; +"Common.RestartRequired" = "إعادة التشغيل مطلوب"; +"Common.RestartNow" = "إعادة التشغيل الآن"; +"Common.OpenTelegram" = "افتح Telegram"; +"Common.UseTelegramForPremium" = "يُرجى ملاحظة أنه للحصول على Telegram Premium، يجب عليك استخدام تطبيق تيليجرام الرسمي. بمجرد حصولك على Telegram Premium، ستصبح جميع ميزاته متاحة في Swiftgram."; + +"Message.HoldToShowOrReport" = "اضغط للعرض أو الإبلاغ."; + +"Auth.AccountBackupReminder" = "تأكد من أن لديك طريقة الوصول إلى النسخ الاحتياطي. حافظ على شريحة SIM للرسائل القصيرة أو جلسة إضافية لتسجيل الدخول لتجنب أن تكون مغفلة."; +"Auth.UnofficialAppCodeTitle" = "يمكنك الحصول على الرمز فقط من خلال التطبيق الرسمي"; + +"Settings.SmallReactions" = "ردود أفعال صغيرة"; +"Settings.HideReactions" = "إخفاء الردود"; + +"ContextMenu.SaveToCloud" = "الحفظ في السحابة"; +"ContextMenu.SelectFromUser" = "حدد من المؤلف"; + +"Settings.ContextMenu" = "قائمة السياق"; +"Settings.ContextMenu.Notice" = "المدخلات المعطلة ستكون متوفرة في القائمة الفرعية \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "خيارات التمرير لقائمة المحادثة"; +"Settings.DeleteChatSwipeOption" = "اسحب لحذف المحادثة"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "اسحب للقناة الغير مقروءة التالية"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "اسحب للموضوع التالي"; +"Settings.GalleryCamera" = "الكاميرا في معرض الصور"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "زر \"%@\""; +"Settings.SnapDeletionEffect" = "تأثيرات حذف الرسالة"; + +"Settings.Stickers.Size" = "مقاس"; +"Settings.Stickers.Timestamp" = "إظهار الطابع الزمني"; + +"Settings.RecordingButton" = "زر التسجيل الصوتي"; + +"Settings.DefaultEmojisFirst" = "الأفضلية للرموز التعبيرية الافتراضية"; +"Settings.DefaultEmojisFirst.Notice" = "عرض الرموز التعبيرية الافتراضية قبل الرموز المتميزة في لوحة المفاتيح"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "تم إنشاؤه: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "انضم %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "مسجل"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "اضغط مزدوجًا لتحرير الرسالة"; + +"Settings.wideChannelPosts" = "المنشورات الواسعة في القنوات"; +"Settings.ForceEmojiTab" = "لوحة مفاتيح الرموز التعبيرية افتراضيًا"; + +"Settings.forceBuiltInMic" = "فرض استخدام ميكروفون الجهاز"; +"Settings.forceBuiltInMic.Notice" = "إذا تم تمكينه، سيستخدم التطبيق فقط ميكروفون الجهاز حتى لو كانت سماعات الرأس متصلة."; + +"Settings.hideChannelBottomButton" = "إخفاء الزر في اسفل القناة"; diff --git a/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..58e0b20f91b --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Configuració del Contingut"; + +"Settings.Tabs.Header" = "PESTANYES"; +"Settings.Tabs.HideTabBar" = "Amagar barra de pestanyes"; +"Settings.Tabs.ShowContacts" = "Mostrar Pestanya de Contactes"; +"Settings.Tabs.ShowNames" = "Mostrar noms de les pestanyes"; + +"Settings.Folders.BottomTab" = "Carpetes a la part inferior"; +"Settings.Folders.BottomTabStyle" = "Bottom Folders Style"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Amaga \"%@\""; +"Settings.Folders.RememberLast" = "Obrir l'última carpeta"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram obrirà l'última carpeta utilitzada després de reiniciar o canviar de compte."; + +"Settings.Folders.CompactNames" = "Espaiat més petit"; +"Settings.Folders.AllChatsTitle" = "Títol \"Tots els xats\""; +"Settings.Folders.AllChatsTitle.short" = "Curt"; +"Settings.Folders.AllChatsTitle.long" = "Llarg"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Per defecte"; + + +"Settings.ChatList.Header" = "LLISTA DE XATS"; +"Settings.CompactChatList" = "Llista de xats compacta"; + +"Settings.Profiles.Header" = "PERFILS"; + +"Settings.Stories.Hide" = "Amagar Històries"; +"Settings.Stories.WarnBeforeView" = "Preguntar abans de veure"; +"Settings.Stories.DisableSwipeToRecord" = "Desactivar lliscar per enregistrar"; + +"Settings.Translation.QuickTranslateButton" = "Botó de Traducció Ràpida"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Veure Història?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ PODRÀ VEURE que has vist la seva Història."; +"Stories.Warning.NoticeStealth" = "%@ no podrà veure que has vist la seva Història."; + +"Settings.Photo.Quality.Notice" = "Qualitat de les fotos sortints i històries de fotos."; +"Settings.Photo.SendLarge" = "Enviar fotos grans"; +"Settings.Photo.SendLarge.Notice" = "Incrementar el límit de mida en imatges comprimides a 2560px."; + +"Settings.VideoNotes.Header" = "VÍDEOS RODONS"; +"Settings.VideoNotes.StartWithRearCam" = "Començar amb càmera posterior"; + +"Settings.CustomColors.Header" = "COLORS DEL COMPTE"; +"Settings.CustomColors.Saturation" = "SATURACIÓ"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Estableix la saturació a 0%% per desactivar els colors del compte."; + +"Settings.UploadsBoost" = "Millora de càrregues"; +"Settings.DownloadsBoost" = "Millora de baixades"; +"Settings.DownloadsBoost.none" = "Desactivat"; +"Settings.DownloadsBoost.medium" = "Mitjà"; +"Settings.DownloadsBoost.maximum" = "Màxim"; + +"Settings.ShowProfileID" = "Mostrar ID de perfil"; +"Settings.ShowDC" = "Mostrar Data Center"; +"Settings.ShowCreationDate" = "Mostrar Data de Creació de Xat"; +"Settings.ShowCreationDate.Notice" = "La data de creació pot ser desconeguda per alguns xats."; + +"Settings.ShowRegDate" = "Mostra la data d'inscripció"; +"Settings.ShowRegDate.Notice" = "La data d'inscripció és aproximada."; + +"Settings.SendWithReturnKey" = "Enviar amb clau \"retorn\""; +"Settings.HidePhoneInSettingsUI" = "Amagar telèfon en la interfície d'ajustos"; +"Settings.HidePhoneInSettingsUI.Notice" = "Això només amagarà el teu número de telèfon de la interfície d'ajustos. Per amagar-lo als altres, ves a Privadesa i Seguretat."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Si no hi ha en 5 segons"; + +"ProxySettings.UseSystemDNS" = "Utilitzar DNS del sistema"; +"ProxySettings.UseSystemDNS.Notice" = "Utilitzar DNS del sistema per evitar el temps d'espera si no tens accés a Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "No **necessites** %@!"; +"Common.RestartRequired" = "Reinici requerit"; +"Common.RestartNow" = "Reiniciar Ara"; +"Common.OpenTelegram" = "Obrir Telegram"; +"Common.UseTelegramForPremium" = "Recorda que per obtenir Telegram Premium, has d'utilitzar l'aplicació oficial de Telegram. Un cop hagis obtingut Telegram Premium, totes les seves funcions estaran disponibles a Swiftgram."; + +"Message.HoldToShowOrReport" = "Mantingues per Mostrar o Informar."; + +"Auth.AccountBackupReminder" = "Assegura't de tenir un mètode d'accés de reserva. Mantingues un SIM per a SMS o una sessió addicional registrada per evitar quedar bloquejat."; +"Auth.UnofficialAppCodeTitle" = "Només pots obtenir el codi amb l'aplicació oficial"; + +"Settings.SmallReactions" = "Petites reaccions"; +"Settings.HideReactions" = "Amaga les reaccions"; + +"ContextMenu.SaveToCloud" = "Desar al Núvol"; +"ContextMenu.SelectFromUser" = "Seleccionar de l'Autor"; + +"Settings.ContextMenu" = "MENÚ CONTEXTUAL"; +"Settings.ContextMenu.Notice" = "Les entrades desactivades estaran disponibles al submenú \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Opcions desplaçament de la llista de xats"; +"Settings.DeleteChatSwipeOption" = "Desplaceu-vos per esborrar la conversa"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Arrossega cap al següent canal no llegit"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Arrosega cap al següent tema"; +"Settings.GalleryCamera" = "Càmera a la galeria"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Botó"; +"Settings.SnapDeletionEffect" = "Efectes d'eliminació de missatges"; + +"Settings.Stickers.Size" = "GRANOR"; +"Settings.Stickers.Timestamp" = "Mostra l'estona"; + +"Settings.RecordingButton" = "Botó d'enregistrament de veu"; + +"Settings.DefaultEmojisFirst" = "Prioritzar emojis estàndard"; +"Settings.DefaultEmojisFirst.Notice" = "Mostra emojis estàndard abans que premium al teclat emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "creada: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Unida a %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Inscrit"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Toqueu dues vegades per editar el missatge"; + +"Settings.wideChannelPosts" = "Entrades àmplies als canals"; +"Settings.ForceEmojiTab" = "Teclat d'emojis per defecte"; + +"Settings.forceBuiltInMic" = "Força el Micròfon del Dispositiu"; +"Settings.forceBuiltInMic.Notice" = "Si està habilitat, l'aplicació utilitzarà només el micròfon del dispositiu encara que estiguin connectats els auriculars."; + +"Settings.hideChannelBottomButton" = "Amaga el panell inferior del canal"; diff --git a/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..a3a5f890b82 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Nastavení obsahu"; + +"Settings.Tabs.Header" = "ZÁLOŽKY"; +"Settings.Tabs.HideTabBar" = "Skrýt záložku"; +"Settings.Tabs.ShowContacts" = "Zobrazit záložku kontaktů"; +"Settings.Tabs.ShowNames" = "Zobrazit názvy záložek"; + +"Settings.Folders.BottomTab" = "Složky dole"; +"Settings.Folders.BottomTabStyle" = "Styl dolní složky"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Skrýt \"%@\""; +"Settings.Folders.RememberLast" = "Otevřít poslední složku"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram otevře poslední použitou složku po restartu nebo přepnutí účtu."; + +"Settings.Folders.CompactNames" = "Menší vzdálenost"; +"Settings.Folders.AllChatsTitle" = "Název \"Všechny chaty\""; +"Settings.Folders.AllChatsTitle.short" = "Krátký"; +"Settings.Folders.AllChatsTitle.long" = "Dlouhá"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Výchozí"; + + +"Settings.ChatList.Header" = "CHAT SEZNAM"; +"Settings.CompactChatList" = "Kompaktní seznam chatu"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Skrýt příběhy"; +"Settings.Stories.WarnBeforeView" = "Upozornit před zobrazením"; +"Settings.Stories.DisableSwipeToRecord" = "Zakázat přejetí prstem pro nahrávání"; + +"Settings.Translation.QuickTranslateButton" = "Tlačítko pro rychlý překlad"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Zobrazit příběh?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ BUDE VIDĚT, že jste si prohlédl jejich příběh."; +"Stories.Warning.NoticeStealth" = "%@ bude moci vidět, že jste si prohlédl jejich příběh."; + +"Settings.Photo.Quality.Notice" = "Kvalita odchozích fotografií a foto-příběhů."; +"Settings.Photo.SendLarge" = "Poslat velké fotografie"; +"Settings.Photo.SendLarge.Notice" = "Zvýšit limit velikosti komprimovaných obrázků na 2560px."; + +"Settings.VideoNotes.Header" = "KRUHOVÁ VIDEA"; +"Settings.VideoNotes.StartWithRearCam" = "Začít s zadní kamerou"; + +"Settings.CustomColors.Header" = "BARVY ÚČTU"; +"Settings.CustomColors.Saturation" = "SYTOST"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Nastavit sytost na 0%% pro vypnutí barev účtu."; + +"Settings.UploadsBoost" = "Zrychlení nahrávání"; +"Settings.DownloadsBoost" = "Zrychlení stahování"; +"Settings.DownloadsBoost.none" = "Vypnuto"; +"Settings.DownloadsBoost.medium" = "Střední"; +"Settings.DownloadsBoost.maximum" = "Maximální"; + +"Settings.ShowProfileID" = "Zobrazit ID profilu"; +"Settings.ShowDC" = "Zobrazit Data Center"; +"Settings.ShowCreationDate" = "Zobrazit datum vytvoření chatu"; +"Settings.ShowCreationDate.Notice" = "Datum vytvoření chatu může být neznámé pro některé chaty."; + +"Settings.ShowRegDate" = "Zobrazit datum registrace"; +"Settings.ShowRegDate.Notice" = "Datum registrace je přibližné."; + +"Settings.SendWithReturnKey" = "Poslat klávesou \"enter\""; +"Settings.HidePhoneInSettingsUI" = "Skrýt telefon v nastavení"; +"Settings.HidePhoneInSettingsUI.Notice" = "Toto skryje vaše telefonní číslo pouze v nastavení rozhraní. Chcete-li je skryt před ostatními, přejděte na Soukromí a bezpečnost."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Zamknout za 5 sekund"; + +"ProxySettings.UseSystemDNS" = "Použít systémové DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Použít systémové DNS k obejití časového limitu, pokud nemáte přístup k Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Nepotřebujete **%@**!"; +"Common.RestartRequired" = "Vyžadován restart"; +"Common.RestartNow" = "Restartovat nyní"; +"Common.OpenTelegram" = "Otevřít Telegram"; +"Common.UseTelegramForPremium" = "Vezměte prosím na vědomí, že abyste získali Premium, musíte použít oficiální aplikaci Telegram . Jakmile získáte Telegram Premium, všechny jeho funkce budou k dispozici ve Swiftgramu."; + +"Message.HoldToShowOrReport" = "Podržte pro zobrazení nebo nahlášení."; + +"Auth.AccountBackupReminder" = "Ujistěte se, že máte záložní přístupovou metodu. Uchovávejte SIM pro SMS nebo další přihlášenou relaci, abyste předešli zamčení."; +"Auth.UnofficialAppCodeTitle" = "Kód můžete získat pouze s oficiální aplikací"; + +"Settings.SmallReactions" = "Malé reakce"; +"Settings.HideReactions" = "Skrýt reakce"; + +"ContextMenu.SaveToCloud" = "Uložit do cloudu"; +"ContextMenu.SelectFromUser" = "Vybrat od autora"; + +"Settings.ContextMenu" = "KONTEXTOVÉ MENU"; +"Settings.ContextMenu.Notice" = "Zakázané položky budou dostupné v podmenu \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Možnosti potáhnutí v seznamu chatu"; +"Settings.DeleteChatSwipeOption" = "Přejeďte pro smazání chatu"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Táhnout na další nepřečtený kanál"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Přetáhněte na další téma"; +"Settings.GalleryCamera" = "Fotoaparát v galerii"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Tlačítko \"%@\""; +"Settings.SnapDeletionEffect" = "Účinky odstranění zpráv"; + +"Settings.Stickers.Size" = "VELIKOST"; +"Settings.Stickers.Timestamp" = "Zobrazit časové razítko"; + +"Settings.RecordingButton" = "Tlačítko nahrávání hlasu"; + +"Settings.DefaultEmojisFirst" = "Upřednostněte standardní emoji"; +"Settings.DefaultEmojisFirst.Notice" = "Zobrazit standardní emoji před prémiovými na klávesnici s emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "vytvořeno: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Připojeno k %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrováno"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dvojitým klepnutím upravte zprávu"; + +"Settings.wideChannelPosts" = "Široké příspěvky ve skupinách"; +"Settings.ForceEmojiTab" = "Emoji klávesnice ve výchozím nastavení"; + +"Settings.forceBuiltInMic" = "Vynutit vestavěný mikrofon zařízení"; +"Settings.forceBuiltInMic.Notice" = "Pokud je povoleno, aplikace použije pouze mikrofon zařízení, i když jsou připojeny sluchátka."; + +"Settings.hideChannelBottomButton" = "Skrýt panel dolního kanálu"; diff --git a/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..3dc63d65b9d --- /dev/null +++ b/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Indholdindstillinger"; + +"Settings.Tabs.Header" = "Tabs"; +"Settings.Tabs.HideTabBar" = "Skjul Tabbjælke"; +"Settings.Tabs.ShowContacts" = "Kontakte Tab anzeigen"; +"Settings.Tabs.ShowNames" = "Tabnamen anzeigen"; + +"Settings.Folders.BottomTab" = "Ordner - unten"; +"Settings.Folders.BottomTabStyle" = "Bundmapper Stil"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Skjul \"%@\""; +"Settings.Folders.RememberLast" = "Åbn sidste mappe"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram vil åbne den sidst brugte mappe efter genstart eller konto skift."; + +"Settings.Folders.CompactNames" = "Mindre afstand"; +"Settings.Folders.AllChatsTitle" = "\"Alle Chats\" titel"; +"Settings.Folders.AllChatsTitle.short" = "Kort"; +"Settings.Folders.AllChatsTitle.long" = "Lang"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standard"; + + +"Settings.ChatList.Header" = "CHAT LISTE"; +"Settings.CompactChatList" = "Kompakt Chatliste"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Skjul Historier"; +"Settings.Stories.WarnBeforeView" = "Spørg før visning"; +"Settings.Stories.DisableSwipeToRecord" = "Deaktiver swipe for at optage"; + +"Settings.Translation.QuickTranslateButton" = "Schnellübersetzen-Schaltfläche"; + +"Stories.Warning.Author" = "Forfatter"; +"Stories.Warning.ViewStory" = "Se Historie?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ VIL KUNNE SE at du har set deres Historie."; +"Stories.Warning.NoticeStealth" = "%@ Vil ikke kunne se, at du har set deres Historie."; + +"Settings.Photo.Quality.Notice" = "Kvalitet af udgående fotos og foto-historier."; +"Settings.Photo.SendLarge" = "Send store fotos"; +"Settings.Photo.SendLarge.Notice" = "Forøg sidestørrelsesgrænsen på komprimerede billeder til 2560px."; + +"Settings.VideoNotes.Header" = "RUNDE VIDEOS"; +"Settings.VideoNotes.StartWithRearCam" = "Starte mit umgedrehter Kamera"; + +"Settings.CustomColors.Header" = "KONTOFARVER"; +"Settings.CustomColors.Saturation" = "MÆTNING"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Indstil mætning til 0%% for at deaktivere konto farver."; + +"Settings.UploadsBoost" = "Upload Boost"; +"Settings.DownloadsBoost" = "Download Boost"; +"Settings.DownloadsBoost.none" = "Deaktiveret"; +"Settings.DownloadsBoost.medium" = "Mellem"; +"Settings.DownloadsBoost.maximum" = "Maksimum"; + +"Settings.ShowProfileID" = "Profil-ID anzeigen"; +"Settings.ShowDC" = "Vis Datacenter"; +"Settings.ShowCreationDate" = "Vis Chattens Oprettelsesdato"; +"Settings.ShowCreationDate.Notice" = "Oprettelsesdatoen kan være ukendt for nogle chats."; + +"Settings.ShowRegDate" = "Vis Registreringsdato"; +"Settings.ShowRegDate.Notice" = "Registreringsdatoen er omtrentlig."; + +"Settings.SendWithReturnKey" = "Send med \"return\" tasten"; +"Settings.HidePhoneInSettingsUI" = "Telefon in den Einstellungen ausblenden"; +"Settings.HidePhoneInSettingsUI.Notice" = "Deine Nummer wird nur in der Benutzeroberfläche versteckt. Um sie vor anderen zu verbergen, verwende bitte die Privatsphäre-Einstellungen."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Hvis væk i 5 sekunder"; + +"ProxySettings.UseSystemDNS" = "Brug system DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Brug system DNS for at omgå timeout hvis du ikke har adgang til Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Du **behøver ikke** %@!"; +"Common.RestartRequired" = "Genstart krævet"; +"Common.RestartNow" = "Genstart Nu"; +"Common.OpenTelegram" = "Åben Telegram"; +"Common.UseTelegramForPremium" = "Bemærk venligst, at for at få Telegram Premium skal du bruge den officielle Telegram app. Når du har fået Telegram Premium, vil alle dens funktioner blive tilgængelige i Swiftgram."; + +"Message.HoldToShowOrReport" = "Hold for at Vise eller Rapportere."; + +"Auth.AccountBackupReminder" = "Sørg for, at du har en backup adgangsmetode. Behold et SIM til SMS eller en ekstra session logget ind for at undgå at blive låst ude."; +"Auth.UnofficialAppCodeTitle" = "Du kan kun få koden med den officielle app"; + +"Settings.SmallReactions" = "Små reaktioner"; +"Settings.HideReactions" = "Skjul Reaktioner"; + +"ContextMenu.SaveToCloud" = "In Cloud speichern"; +"ContextMenu.SelectFromUser" = "Vælg fra Forfatter"; + +"Settings.ContextMenu" = "KONTEKSTMENU"; +"Settings.ContextMenu.Notice" = "Deaktiverede indgange vil være tilgængelige i \"Swiftgram\" undermenuen."; + + +"Settings.ChatSwipeOptions" = "Chat List Swipe Options"; +"Settings.DeleteChatSwipeOption" = "Svejp for at slette chat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Træk til Næste U’læst Kanal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Træk for at gå til næste emne"; +"Settings.GalleryCamera" = "Kamera i Galleri"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Knap"; +"Settings.SnapDeletionEffect" = "Besked Sletnings Effekter"; + +"Settings.Stickers.Size" = "STØRRELSE"; +"Settings.Stickers.Timestamp" = "Vis tidsstempel"; + +"Settings.RecordingButton" = "Lydoptageknap"; + +"Settings.DefaultEmojisFirst" = "Prioriter standard emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Vis standard emojis før premium i emoji-tastaturet"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "oprettet: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Tilmeldt %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registreret"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dobbelt tryk for at redigere besked"; + +"Settings.wideChannelPosts" = "Brede indlæg i kanaler"; +"Settings.ForceEmojiTab" = "Emoji-tastatur som standard"; + +"Settings.forceBuiltInMic" = "Tving enhedsmikrofon"; +"Settings.forceBuiltInMic.Notice" = "Hvis aktiveret, vil appen kun bruge enhedens mikrofon, selvom hovedtelefoner er tilsluttet."; + +"Settings.hideChannelBottomButton" = "Skjul Kanal Bund Panel"; diff --git a/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..8dfd51d57f6 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Inhaltliche Einstellungen"; + +"Settings.Tabs.Header" = "Tabs"; +"Settings.Tabs.HideTabBar" = "Tab-Leiste ausblenden"; +"Settings.Tabs.ShowContacts" = "Kontakte Tab anzeigen"; +"Settings.Tabs.ShowNames" = "Tabnamen anzeigen"; + +"Settings.Folders.BottomTab" = "Ordner unten"; +"Settings.Folders.BottomTabStyle" = "Untere Ordner-Stil"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Verberge \"%@\""; +"Settings.Folders.RememberLast" = "Letzten Ordner öffnen"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram wird den zuletzt genutzten Order öffnen, wenn du den Account wechselst oder die App neustartest"; + +"Settings.Folders.CompactNames" = "Kleinerer Abstand"; +"Settings.Folders.AllChatsTitle" = "Titel \"Alle Chats\""; +"Settings.Folders.AllChatsTitle.short" = "Kurze"; +"Settings.Folders.AllChatsTitle.long" = "Lang"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standard"; + + +"Settings.ChatList.Header" = "CHAT LISTE"; +"Settings.CompactChatList" = "Kompakte Chat-Liste"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Stories verbergen"; +"Settings.Stories.WarnBeforeView" = "Vor dem Ansehen fragen"; +"Settings.Stories.DisableSwipeToRecord" = "Zum aufnehmen wischen deaktivieren"; + +"Settings.Translation.QuickTranslateButton" = "Schnellübersetzen-Button"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Story ansehen?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ wird sehen können, dass du die Story angesehen hast."; +"Stories.Warning.NoticeStealth" = "%@ wird nicht sehen können, dass du die Story angesehen hast."; + +"Settings.Photo.Quality.Notice" = "Qualität der gesendeten Fotos und Fotostorys"; +"Settings.Photo.SendLarge" = "Sende große Fotos"; +"Settings.Photo.SendLarge.Notice" = "Seitenlimit für komprimierte Bilder auf 2560px erhöhen"; + +"Settings.VideoNotes.Header" = "RUNDE VIDEOS"; +"Settings.VideoNotes.StartWithRearCam" = "Starte mit umgedrehter Kamera"; + +"Settings.CustomColors.Header" = "ACCOUNT FARBEN"; +"Settings.CustomColors.Saturation" = "SÄTTIGUNG"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Setze Sättigung auf 0%% um Kontofarben zu deaktivieren"; + +"Settings.UploadsBoost" = "Upload Beschleuniger"; +"Settings.DownloadsBoost" = "Download Beschleuniger"; +"Settings.DownloadsBoost.none" = "Deaktiviert"; +"Settings.DownloadsBoost.medium" = "Mittel"; +"Settings.DownloadsBoost.maximum" = "Maximum"; + +"Settings.ShowProfileID" = "Profil-ID anzeigen"; +"Settings.ShowDC" = "Data Center anzeigen"; +"Settings.ShowCreationDate" = "Chat-Erstellungsdatum anzeigen"; +"Settings.ShowCreationDate.Notice" = "Das Erstellungsdatum kann für einige Chats unbekannt sein."; + +"Settings.ShowRegDate" = "Anmeldedatum anzeigen"; +"Settings.ShowRegDate.Notice" = "Das Registrierungsdatum ist ungefähr."; + +"Settings.SendWithReturnKey" = "Mit \"Enter\" senden"; +"Settings.HidePhoneInSettingsUI" = "Telefon in den Einstellungen ausblenden"; +"Settings.HidePhoneInSettingsUI.Notice" = "Deine Nummer wird nur in der Benutzeroberfläche versteckt. Um sie vor anderen zu verbergen, verwende bitte die Privatsphäre-Einstellungen."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Falls 5 Sekunden inaktiv"; + +"ProxySettings.UseSystemDNS" = "Benutze System DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Benutze System DNS um Timeout zu umgehen, wenn du keinen Zugriff auf Google DNS hast"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Du brauchst %@ nicht!"; +"Common.RestartRequired" = "Benötigt Neustart"; +"Common.RestartNow" = "Jetzt neustarten"; +"Common.OpenTelegram" = "Telegram öffnen"; +"Common.UseTelegramForPremium" = "Bitte beachten Sie, dass Sie die offizielle Telegram-App verwenden müssen, um Telegram Premium zu erhalten. Sobald Sie Telegram Premium erhalten haben, werden alle Funktionen in Swiftgram verfügbar."; + +"Message.HoldToShowOrReport" = "Halten, zum Ansehen oder melden."; + +"Auth.AccountBackupReminder" = "Stelle sicher, dass du eine weiter Möglichkeit hast auf den Account zuzugreifen. Behalte die SIM Karte im SMS zum Login empfangen zu können oder nutze weitere Apps/Geräte mit einer aktive Sitzung deines Accounts."; +"Auth.UnofficialAppCodeTitle" = "Du kannst den Code nur mit der offiziellen App erhalten"; + +"Settings.SmallReactions" = "Kleine Reaktionen"; +"Settings.HideReactions" = "Verberge Reaktionen"; + +"ContextMenu.SaveToCloud" = "In Cloud speichern"; +"ContextMenu.SelectFromUser" = "Vom Autor auswählen"; + +"Settings.ContextMenu" = "KONTEXTMENÜ"; +"Settings.ContextMenu.Notice" = "Deaktivierte Einträge sind im 'Swiftgram'-Untermenü verfügbar."; + + +"Settings.ChatSwipeOptions" = "Chatlisten-Wisch-Optionen"; +"Settings.DeleteChatSwipeOption" = "Wischen zum Löschen des Chats"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Ziehen zum nächsten Kanal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Ziehen Sie zum nächsten Thema"; +"Settings.GalleryCamera" = "Kamera in der Galerie"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Schaltfläche"; +"Settings.SnapDeletionEffect" = "Nachrichtenlösch-Effekte"; + +"Settings.Stickers.Size" = "GRÖSSE"; +"Settings.Stickers.Timestamp" = "Zeitstempel anzeigen"; + +"Settings.RecordingButton" = "Sprachaufnahme-Button"; + +"Settings.DefaultEmojisFirst" = "Priorisieren Sie Standard-Emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Zeigen Sie Standard-Emojis vor Premium-Emojis in der Emoji-Tastatur"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "erstellt: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Beigetreten am %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registriert"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Doppeltippen, um Nachricht zu bearbeiten"; + +"Settings.wideChannelPosts" = "Breite Beiträge in Kanälen"; +"Settings.ForceEmojiTab" = "Emoji-Tastatur standardmäßig"; + +"Settings.forceBuiltInMic" = "Erzwinge Geräte-Mikrofon"; +"Settings.forceBuiltInMic.Notice" = "Wenn aktiviert, verwendet die App nur das Geräte-Mikrofon, auch wenn Kopfhörer angeschlossen sind."; + +"Settings.hideChannelBottomButton" = "Kanalunteres Bedienfeld ausblenden"; diff --git a/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..64e022e22ce --- /dev/null +++ b/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Ρυθμίσεις Περιεχομένου"; + +"Settings.Tabs.Header" = "TABS"; +"Settings.Tabs.HideTabBar" = "Απόκρυψη γραμμής καρτελών"; +"Settings.Tabs.ShowContacts" = "Εμφάνιση Καρτέλας Επαφών"; +"Settings.Tabs.ShowNames" = "Show Tab Names"; + +"Settings.Folders.BottomTab" = "Φάκελοι στο κάτω μέρος"; +"Settings.Folders.BottomTabStyle" = "Ύφος Κάτω Φακέλων"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Απόκρυψη \"%@\""; +"Settings.Folders.RememberLast" = "Άνοιγμα Τελευταίου Φακέλου"; +"Settings.Folders.RememberLast.Notice" = "Το Swiftgram θα ανοίξει τον τελευταίο φάκελο όταν επανεκκινήσετε την εφαρμογή ή αλλάξετε λογαριασμούς."; + +"Settings.Folders.CompactNames" = "Μικρότερη απόσταση"; +"Settings.Folders.AllChatsTitle" = "\"Όλες οι συνομιλίες\" τίτλος"; +"Settings.Folders.AllChatsTitle.short" = "Σύντομο"; +"Settings.Folders.AllChatsTitle.long" = "Εκτενές"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Προεπιλογή"; + + +"Settings.ChatList.Header" = "ΚΑΤΑΛΟΓΟΣ ΤΥΠΟΥ"; +"Settings.CompactChatList" = "Συμπαγής Λίστα Συνομιλίας"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Απόκρυψη Ιστοριών"; +"Settings.Stories.WarnBeforeView" = "Ερώτηση Πριν Την Προβολή"; +"Settings.Stories.DisableSwipeToRecord" = "Απενεργοποίηση ολίσθησης για εγγραφή"; + +"Settings.Translation.QuickTranslateButton" = "Γρήγορη μετάφραση κουμπί"; + +"Stories.Warning.Author" = "Συγγραφέας"; +"Stories.Warning.ViewStory" = "Προβολή Ιστορίας?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ ΘΑ ΠΡΕΠΕΙ ΝΑ ΒΛΕΠΕ ότι έχετε δει την Ιστορία τους."; +"Stories.Warning.NoticeStealth" = "%@ δεν θα είναι σε θέση να δείτε ότι έχετε δει την Ιστορία τους."; + +"Settings.Photo.Quality.Notice" = "Ποιότητα των ανεβασμένων φωτογραφιών και ιστοριών."; +"Settings.Photo.SendLarge" = "Αποστολή Μεγάλων Φωτογραφιών"; +"Settings.Photo.SendLarge.Notice" = "Αυξήστε το πλευρικό όριο στις συμπιεσμένες εικόνες στα 2560px."; + +"Settings.VideoNotes.Header" = "ΤΡΟΠΟΣ ΒΙΝΤΕΟ"; +"Settings.VideoNotes.StartWithRearCam" = "Έναρξη με πίσω κάμερα"; + +"Settings.CustomColors.Header" = "ΧΡΩΜΑΤΑ ΛΟΓΑΡΙΑΣΜΟΥ"; +"Settings.CustomColors.Saturation" = "ΑΣΦΑΛΙΣΗ"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Ορίστε σε 0%% για να απενεργοποιήσετε τα χρώματα του λογαριασμού."; + +"Settings.UploadsBoost" = "Ενίσχυση Αποστολής"; +"Settings.DownloadsBoost" = "Ενίσχυση Λήψης"; +"Settings.DownloadsBoost.none" = "Απενεργοποιημένο"; +"Settings.DownloadsBoost.medium" = "Μεσαίο"; +"Settings.DownloadsBoost.maximum" = "Μέγιστο"; + +"Settings.ShowProfileID" = "Εμφάνιση Αναγνωριστικού Προφίλ"; +"Settings.ShowDC" = "Εμφάνιση Κέντρου Δεδομένων"; +"Settings.ShowCreationDate" = "Εμφάνιση Ημερομηνίας Δημιουργίας Συνομιλίας"; +"Settings.ShowCreationDate.Notice" = "Η ημερομηνία δημιουργίας μπορεί να είναι άγνωστη για μερικές συνομιλίες."; + +"Settings.ShowRegDate" = "Εμφάνιση Ημερομηνίας Εγγραφής"; +"Settings.ShowRegDate.Notice" = "Η ημερομηνία εγγραφής είναι κατά προσέγγιση."; + +"Settings.SendWithReturnKey" = "Αποστολή με κλειδί \"επιστροφή\""; +"Settings.HidePhoneInSettingsUI" = "Απόκρυψη τηλεφώνου στις ρυθμίσεις"; +"Settings.HidePhoneInSettingsUI.Notice" = "Αυτό θα κρύψει μόνο τον αριθμό τηλεφώνου σας από τη διεπαφή ρυθμίσεων. Για να τον αποκρύψετε από άλλους, μεταβείτε στο Privacy and Security."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Εάν είναι μακριά για 5 δευτερόλεπτα"; + +"ProxySettings.UseSystemDNS" = "Χρήση συστήματος DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Χρησιμοποιήστε το σύστημα DNS για να παρακάμψετε το χρονικό όριο αν δεν έχετε πρόσβαση στο Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Du **brauchst kein** %@!"; +"Common.RestartRequired" = "Απαιτείται επανεκκίνηση"; +"Common.RestartNow" = "Επανεκκίνηση Τώρα"; +"Common.OpenTelegram" = "Άνοιγμα Telegram"; +"Common.UseTelegramForPremium" = "Παρακαλώ σημειώστε ότι για να πάρετε Telegram Premium, θα πρέπει να χρησιμοποιήσετε την επίσημη εφαρμογή Telegram. Μόλις λάβετε Telegram Premium, όλα τα χαρακτηριστικά του θα είναι διαθέσιμα στο Swiftgram."; + +"Message.HoldToShowOrReport" = "Κρατήστε για προβολή ή αναφορά."; + +"Auth.AccountBackupReminder" = "Βεβαιωθείτε ότι έχετε μια μέθοδο πρόσβασης αντιγράφων ασφαλείας. Κρατήστε μια SIM για SMS ή μια πρόσθετη συνεδρία συνδεδεμένη για να αποφύγετε να κλειδωθεί."; +"Auth.UnofficialAppCodeTitle" = "Μπορείτε να πάρετε τον κωδικό μόνο με επίσημη εφαρμογή"; + +"Settings.SmallReactions" = "Μικρές Αντιδράσεις"; +"Settings.HideReactions" = "Απόκρυψη Αντιδράσεων"; + +"ContextMenu.SaveToCloud" = "Αποθήκευση στο σύννεφο"; +"ContextMenu.SelectFromUser" = "Επιλέξτε από τον Συγγραφέα"; + +"Settings.ContextMenu" = "KONTEXTMENÜ"; +"Settings.ContextMenu.Notice" = "Deaktivierte Einträge sind im 'Swiftgram'-Untermenü verfügbar."; + + +"Settings.ChatSwipeOptions" = "Επιλογές Συρσίματος Λίστας Συνομιλίας"; +"Settings.DeleteChatSwipeOption" = "Σύρετε για Διαγραφή Συνομιλίας"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Τραβήξτε στο επόμενο μη αναγνωσμένο κανάλι"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Τραβήξτε για το Επόμενο Θέμα"; +"Settings.GalleryCamera" = "Κάμερα στη Γκαλερί"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\" Κουμπί%@\""; +"Settings.SnapDeletionEffect" = "Εφέ Διαγραφής Μηνύματος"; + +"Settings.Stickers.Size" = "ΜΕΓΕΘΟΣ"; +"Settings.Stickers.Timestamp" = "Εμφάνιση Χρονοσήμανσης"; + +"Settings.RecordingButton" = "Πλήκτρο Ηχογράφησης Φωνής"; + +"Settings.DefaultEmojisFirst" = "Δώστε προτεραιότητα στα τυπικά emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Εμφανίστε τυπικά emojis πριν από premium στο πληκτρολόγιο emojis"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "δημιουργήθηκε: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Εντάχθηκε στο %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Εγγεγραμμένος"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Διπλό Πάτημα για Επεξεργασία Μηνύματος"; + +"Settings.wideChannelPosts" = "Πλατείες αναρτήσεις στα κανάλια"; +"Settings.ForceEmojiTab" = "Πληκτρολόγιο Emoji από προεπιλογή"; + +"Settings.forceBuiltInMic" = "Εξαναγκασμός Μικροφώνου Συσκευής"; +"Settings.forceBuiltInMic.Notice" = "Εάν ενεργοποιηθεί, η εφαρμογή θα χρησιμοποιεί μόνο το μικρόφωνο της συσκευής ακόμα και αν είναι συνδεδεμένα ακουστικά."; + +"Settings.hideChannelBottomButton" = "Απόκρυψη Καναλιού Κάτω Πάνελ"; diff --git a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..88f2f43a5d1 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings @@ -0,0 +1,148 @@ +"Settings.ContentSettings" = "Content Settings"; + +"Settings.Tabs.Header" = "TABS"; +"Settings.Tabs.HideTabBar" = "Hide Tab bar"; +"Settings.Tabs.ShowContacts" = "Show Contacts Tab"; +"Settings.Tabs.ShowNames" = "Show Tab Names"; + +"Settings.Folders.BottomTab" = "Folders at Bottom"; +"Settings.Folders.BottomTabStyle" = "Bottom Folders Style"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Hide \"%@\""; +"Settings.Folders.RememberLast" = "Open Last Folder"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram will open the last used folder when you restart the app or switch accounts."; + +"Settings.Folders.CompactNames" = "Smaller spacing"; +"Settings.Folders.AllChatsTitle" = "\"All Chats\" title"; +"Settings.Folders.AllChatsTitle.short" = "Short"; +"Settings.Folders.AllChatsTitle.long" = "Long"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Default"; + + +"Settings.ChatList.Header" = "CHAT LIST"; +"Settings.CompactChatList" = "Compact Chat List"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Hide Stories"; +"Settings.Stories.WarnBeforeView" = "Ask Before Viewing"; +"Settings.Stories.DisableSwipeToRecord" = "Disable Swipe to Record"; + +"Settings.Translation.QuickTranslateButton" = "Quick Translate button"; + +"Stories.Warning.Author" = "Author"; +"Stories.Warning.ViewStory" = "View Story?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ WILL BE ABLE TO SEE that you viewed their Story."; +"Stories.Warning.NoticeStealth" = "%@ will not be able to see that you viewed their Story."; + +"Settings.Photo.Quality.Notice" = "Quality of uploaded photos and stories."; +"Settings.Photo.SendLarge" = "Send Large Photos"; +"Settings.Photo.SendLarge.Notice" = "Increase the side limit on compressed images to 2560px."; + +"Settings.VideoNotes.Header" = "ROUND VIDEOS"; +"Settings.VideoNotes.StartWithRearCam" = "Start with Rear Camera"; + +"Settings.CustomColors.Header" = "ACCOUNT COLORS"; +"Settings.CustomColors.Saturation" = "SATURATION"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Set to 0%% to disable account colors."; + +"Settings.UploadsBoost" = "Upload Boost"; +"Settings.DownloadsBoost" = "Download Boost"; +"Settings.DownloadsBoost.none" = "Disabled"; +"Settings.DownloadsBoost.medium" = "Medium"; +"Settings.DownloadsBoost.maximum" = "Maximum"; + +"Settings.ShowProfileID" = "Show Profile ID"; +"Settings.ShowDC" = "Show Data Center"; +"Settings.ShowCreationDate" = "Show Chat Creation Date"; +"Settings.ShowCreationDate.Notice" = "The creation date may be unknown for some chats."; + +"Settings.ShowRegDate" = "Show Registration Date"; +"Settings.ShowRegDate.Notice" = "The registration date is approximate."; + +"Settings.SendWithReturnKey" = "Send with \"return\" key"; +"Settings.HidePhoneInSettingsUI" = "Hide Phone in Settings"; +"Settings.HidePhoneInSettingsUI.Notice" = "This will only hide your phone number from the settings interface. To hide it from others, go to Privacy and Security."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "If away for 5 seconds"; + +"ProxySettings.UseSystemDNS" = "Use system DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Use system DNS to bypass timeout if you don't have access to Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "You **don't need** %@!"; +"Common.RestartRequired" = "Restart required"; +"Common.RestartNow" = "Restart Now"; +"Common.OpenTelegram" = "Open Telegram"; +"Common.UseTelegramForPremium" = "Please note that to get Telegram Premium, you must use the official Telegram app. Once you have obtained Telegram Premium, all its features will become available in Swiftgram."; + +"Message.HoldToShowOrReport" = "Hold to Show or Report."; + +"Auth.AccountBackupReminder" = "Make sure you have a backup access method. Keep a SIM for SMS or an additional session logged in to avoid being locked out."; +"Auth.UnofficialAppCodeTitle" = "You can get the code only with official app"; + +"Settings.SmallReactions" = "Small Reactions"; +"Settings.HideReactions" = "Hide Reactions"; + +"ContextMenu.SaveToCloud" = "Save to Cloud"; +"ContextMenu.SelectFromUser" = "Select from Author"; + +"Settings.ContextMenu" = "CONTEXT MENU"; +"Settings.ContextMenu.Notice" = "Disabled entries will be available in \"Swiftgram\" sub-menu."; + + +"Settings.ChatSwipeOptions" = "Chat List Swipe Options"; +"Settings.DeleteChatSwipeOption" = "Swipe to Delete Chat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Pull to Next Unread Channel"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Pull to Next Topic"; +"Settings.GalleryCamera" = "Camera in Gallery"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Button"; +"Settings.SnapDeletionEffect" = "Message Deletion Effects"; + +"Settings.Stickers.Size" = "SIZE"; +"Settings.Stickers.Timestamp" = "Show Timestamp"; + +"Settings.RecordingButton" = "Voice Recording Button"; + +"Settings.DefaultEmojisFirst" = "Prioritize standard emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Show standard emojis before premium in emoji keyboard"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "created: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Joined %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registered"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Double-tap to edit message"; + +"Settings.wideChannelPosts" = "Wide posts in channels"; +"Settings.ForceEmojiTab" = "Emoji keyboard by default"; + +"Settings.forceBuiltInMic" = "Force Device Microphone"; +"Settings.forceBuiltInMic.Notice" = "If enabled, app will use only device microphone even if headphones are connected."; + +"Settings.hideChannelBottomButton" = "Hide Channel Bottom Panel"; + +"Settings.CallConfirmation" = "Call Confirmation"; +"Settings.CallConfirmation.Notice" = "Swiftgram will ask for your confirmation before making a call."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Make a Call?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Make a Video Call?"; + +"MutualContact.Label" = "mutual contact"; diff --git a/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..4e1397b0992 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Configuración de contenido"; + +"Settings.Tabs.Header" = "PESTAÑAS"; +"Settings.Tabs.HideTabBar" = "Ocultar barra de pestaña"; +"Settings.Tabs.ShowContacts" = "Mostrar pestaña de Contactos"; +"Settings.Tabs.ShowNames" = "Mostrar nombres de pestañas"; + +"Settings.Folders.BottomTab" = "Carpetas al fondo"; +"Settings.Folders.BottomTabStyle" = "Estilo de carpetas al fondo"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Ocultar \"%@\""; +"Settings.Folders.RememberLast" = "Abrir última carpeta"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram abrirá la última carpeta usada después de reiniciar o cambiar de cuenta"; + +"Settings.Folders.CompactNames" = "Espaciado más pequeño"; +"Settings.Folders.AllChatsTitle" = "Título \"Todos los Chats\""; +"Settings.Folders.AllChatsTitle.short" = "Corto"; +"Settings.Folders.AllChatsTitle.long" = "Largo"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Por defecto"; + + +"Settings.ChatList.Header" = "LISTA DE CHAT"; +"Settings.CompactChatList" = "Lista de Chat de Compacto"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Ocultar Historias"; +"Settings.Stories.WarnBeforeView" = "Preguntar antes de ver"; +"Settings.Stories.DisableSwipeToRecord" = "Desactivar deslizar para grabar"; + +"Settings.Translation.QuickTranslateButton" = "Botón de traducción rápida"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "¿Ver historia?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ PODRÁ VER que viste su historia."; +"Stories.Warning.NoticeStealth" = "%@ no podrá ver que viste su historia."; + +"Settings.Photo.Quality.Notice" = "Calidad de las fotos y foto-historias enviadas"; +"Settings.Photo.SendLarge" = "Enviar fotos grandes"; +"Settings.Photo.SendLarge.Notice" = "Aumentar el límite de tamaño de las imágenes comprimidas a 2560px"; + +"Settings.VideoNotes.Header" = "VIDEOS REDONDOS"; +"Settings.VideoNotes.StartWithRearCam" = "Comenzar con la cámara trasera"; + +"Settings.CustomColors.Header" = "COLORES DE LA CUENTA"; +"Settings.CustomColors.Saturation" = "SATURACIÓN"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Establecer saturación en 0%% para desactivar los colores de la cuenta"; + +"Settings.UploadsBoost" = "Aumento de subida"; +"Settings.DownloadsBoost" = "Aumento de descargas"; +"Settings.DownloadsBoost.none" = "Desactivado"; +"Settings.DownloadsBoost.medium" = "Medio"; +"Settings.DownloadsBoost.maximum" = "Máximo"; + +"Settings.ShowProfileID" = "Mostrar ID del perfil"; +"Settings.ShowDC" = "Mostrar Centro de Datos"; +"Settings.ShowCreationDate" = "Mostrar fecha de creación del chat"; +"Settings.ShowCreationDate.Notice" = "La fecha de creación puede ser desconocida para algunos chats."; + +"Settings.ShowRegDate" = "Mostrar fecha de registro"; +"Settings.ShowRegDate.Notice" = "La fecha de inscripción es aproximada."; + +"Settings.SendWithReturnKey" = "Enviar con la tecla \"regresar\""; +"Settings.HidePhoneInSettingsUI" = "Ocultar número en Ajustes"; +"Settings.HidePhoneInSettingsUI.Notice" = "Tu número estará oculto en la interfaz de ajustes solamente. Ve a la configuración de privacidad para ocultarlo a otros."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Si está ausente durante 5 segundos"; + +"ProxySettings.UseSystemDNS" = "Usar DNS del sistema"; +"ProxySettings.UseSystemDNS.Notice" = "Usa el DNS del sistema para omitir el tiempo de espera si no tienes acceso a Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "¡**No necesitas** %@!"; +"Common.RestartRequired" = "Es necesario reiniciar"; +"Common.RestartNow" = "Reiniciar ahora"; +"Common.OpenTelegram" = "Abrir Telegram"; +"Common.UseTelegramForPremium" = "Ten en cuenta que para obtener Telegram Premium, debes usar la aplicación oficial de Telegram. Una vez que haya obtenido Telegram Premium, todas sus características estarán disponibles en Swiftgram."; + +"Message.HoldToShowOrReport" = "Mantenga presionado para mostrar o reportar."; + +"Auth.AccountBackupReminder" = "Asegúrate de que tienes un método de acceso de copia de seguridad. Mantenga una SIM para SMS o una sesión adicional conectada para evitar ser bloqueada."; +"Auth.UnofficialAppCodeTitle" = "Sólo puedes obtener el código con la app oficial"; + +"Settings.SmallReactions" = "Reacciones pequeñas"; +"Settings.HideReactions" = "Ocultar Reacciones"; + +"ContextMenu.SaveToCloud" = "Guardar en la nube"; +"ContextMenu.SelectFromUser" = "Seleccionar del autor"; + +"Settings.ContextMenu" = "MENÚ CONTEXTUAL"; +"Settings.ContextMenu.Notice" = "Las entradas desactivadas estarán disponibles en el submenú \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Opciones de deslizamiento de la lista de chats"; +"Settings.DeleteChatSwipeOption" = "Deslizar para eliminar chat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Saltar al siguiente canal no leído"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Deslizar para ir al siguiente tema"; +"Settings.GalleryCamera" = "Cámara en galería"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Botón \"%@\""; +"Settings.SnapDeletionEffect" = "Efectos de eliminación de mensajes"; + +"Settings.Stickers.Size" = "TAMAÑO"; +"Settings.Stickers.Timestamp" = "Mostrar marca de tiempo"; + +"Settings.RecordingButton" = "Botón de grabación de voz"; + +"Settings.DefaultEmojisFirst" = "Priorizar emojis estándar"; +"Settings.DefaultEmojisFirst.Notice" = "Mostrar emojis estándar antes que premium en el teclado de emojis"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "creado: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Unido a %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrado"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Doble toque para editar mensaje"; + +"Settings.wideChannelPosts" = "Publicaciones amplias en canales"; +"Settings.ForceEmojiTab" = "Teclado de emojis por defecto"; + +"Settings.forceBuiltInMic" = "Forzar Micrófono del Dispositivo"; +"Settings.forceBuiltInMic.Notice" = "Si está habilitado, la aplicación utilizará solo el micrófono del dispositivo incluso si se conectan auriculares."; + +"Settings.hideChannelBottomButton" = "Ocultar Panel Inferior del Canal"; diff --git a/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..1581d635363 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings @@ -0,0 +1,9 @@ +"Settings.Tabs.Header" = "زبانه ها"; +"Settings.Tabs.ShowContacts" = "نمایش برگه مخاطبین"; +"Settings.VideoNotes.Header" = "فیلم های round"; +"Settings.Tabs.ShowNames" = "نشان دادن برگه اسم ها"; +"Settings.HidePhoneInSettingsUI" = "پنهان کردن شماره موبایل در تنظیمات"; +"Settings.HidePhoneInSettingsUI.Notice" = "شماره شما فقط در رابط کاربری پنهان خواهد شد. برای پنهان کردن آن از دید دیگران ، لطفاً از تنظیمات حریم خصوصی استفاده کنید."; +"Settings.ShowProfileID" = "نمایش ایدی پروفایل"; +"Settings.Translation.QuickTranslateButton" = "دکمه ترجمه سریع"; +"ContextMenu.SaveToCloud" = "ذخیره در فضای ابری"; diff --git a/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..8b97357d72f --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Sisällön Asetukset"; + +"Settings.Tabs.Header" = "VÄLILEHDET"; +"Settings.Tabs.HideTabBar" = "Piilota Välilehtipalkki"; +"Settings.Tabs.ShowContacts" = "Näytä Yhteystiedot-välilehti"; +"Settings.Tabs.ShowNames" = "Näytä välilehtien nimet"; + +"Settings.Folders.BottomTab" = "Kansiot alhaalla"; +"Settings.Folders.BottomTabStyle" = "Alhaalla olevien kansioiden tyyli"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Piilota \"%@\""; +"Settings.Folders.RememberLast" = "Avaa viimeisin kansio"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram avaa viimeksi käytetyn kansion uudelleenkäynnistyksen tai tilin vaihdon jälkeen."; + +"Settings.Folders.CompactNames" = "Pienempi väli"; +"Settings.Folders.AllChatsTitle" = "\"Kaikki chatit\" otsikko"; +"Settings.Folders.AllChatsTitle.short" = "Lyhyt"; +"Settings.Folders.AllChatsTitle.long" = "Pitkä"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Oletus"; + + +"Settings.ChatList.Header" = "CHAT LIST"; +"Settings.CompactChatList" = "Kompakti Keskustelulista"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Piilota Tarinat"; +"Settings.Stories.WarnBeforeView" = "Kysy ennen katsomista"; +"Settings.Stories.DisableSwipeToRecord" = "Poista pyyhkäisy tallennukseen käytöstä"; + +"Settings.Translation.QuickTranslateButton" = "Pikakäännöspainike"; + +"Stories.Warning.Author" = "Tekijä"; +"Stories.Warning.ViewStory" = "Katso Tarina?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ NÄKEE, että olet katsonut heidän Tarinansa."; +"Stories.Warning.NoticeStealth" = "%@ ei näe, että olet katsonut heidän Tarinansa."; + +"Settings.Photo.Quality.Notice" = "Lähtevien valokuvien ja valokuvatarinoiden laatu."; +"Settings.Photo.SendLarge" = "Lähetä suuria valokuvia"; +"Settings.Photo.SendLarge.Notice" = "Suurenna pakattujen kuvien sivurajaa 2560px:ään."; + +"Settings.VideoNotes.Header" = "PYÖREÄT VIDEOT"; +"Settings.VideoNotes.StartWithRearCam" = "Aloita takakameralla"; + +"Settings.CustomColors.Header" = "TILIN VÄRIT"; +"Settings.CustomColors.Saturation" = "KYLLÄISYYS"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Aseta kylläisyys 0%%:iin poistaaksesi tilin värit käytöstä."; + +"Settings.UploadsBoost" = "Latausten tehostus"; +"Settings.DownloadsBoost" = "Latausten tehostus"; +"Settings.DownloadsBoost.none" = "Ei käytössä"; +"Settings.DownloadsBoost.medium" = "Keskitaso"; +"Settings.DownloadsBoost.maximum" = "Maksimi"; + +"Settings.ShowProfileID" = "Näytä profiilin ID"; +"Settings.ShowDC" = "Näytä tietokeskus"; +"Settings.ShowCreationDate" = "Näytä keskustelun luontipäivä"; +"Settings.ShowCreationDate.Notice" = "Keskustelun luontipäivä voi olla tuntematon joillekin keskusteluille."; + +"Settings.ShowRegDate" = "Näytä Rekisteröintipäivä"; +"Settings.ShowRegDate.Notice" = "Rekisteröintipäivä on likimääräinen."; + +"Settings.SendWithReturnKey" = "Lähetä 'paluu'-näppäimellä"; +"Settings.HidePhoneInSettingsUI" = "Piilota puhelin asetuksissa"; +"Settings.HidePhoneInSettingsUI.Notice" = "Tämä piilottaa puhelinnumerosi vain asetukset-käyttöliittymästä. Piilottaaksesi sen muilta, siirry kohtaan Yksityisyys ja Turvallisuus."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Jos poissa 5 sekuntia"; + +"ProxySettings.UseSystemDNS" = "Käytä järjestelmän DNS:ää"; +"ProxySettings.UseSystemDNS.Notice" = "Käytä järjestelmän DNS:ää ohittaaksesi aikakatkaisun, jos sinulla ei ole pääsyä Google DNS:ään"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Et **tarvitse** %@!"; +"Common.RestartRequired" = "Uudelleenkäynnistys vaaditaan"; +"Common.RestartNow" = "Käynnistä uudelleen nyt"; +"Common.OpenTelegram" = "Avaa Telegram"; +"Common.UseTelegramForPremium" = "Huomioi, että saat Telegram Premiumin käyttämällä virallista Telegram-sovellusta. Kun olet hankkinut Telegram Premiumin, kaikki sen ominaisuudet ovat saatavilla Swiftgramissa."; + +"Message.HoldToShowOrReport" = "Pidä esillä näyttääksesi tai ilmoittaaksesi."; + +"Auth.AccountBackupReminder" = "Varmista, että sinulla on varmuuskopio pääsymenetelmästä. Pidä SIM tekstiviestejä varten tai ylimääräinen istunto kirjautuneena välttääksesi lukkiutumisen."; +"Auth.UnofficialAppCodeTitle" = "Koodin voi saada vain virallisella sovelluksella"; + +"Settings.SmallReactions" = "Pienet reaktiot"; +"Settings.HideReactions" = "Piilota reaktiot"; + +"ContextMenu.SaveToCloud" = "Tallenna Pilveen"; +"ContextMenu.SelectFromUser" = "Valitse Tekijältä"; + +"Settings.ContextMenu" = "KONTEKSTIVALIKKO"; +"Settings.ContextMenu.Notice" = "Poistetut kohteet ovat saatavilla 'Swiftgram'-alavalikossa."; + + +"Settings.ChatSwipeOptions" = "Chat List Swipe Options"; +"Settings.DeleteChatSwipeOption" = "Vedä poistaaksesi keskustelu"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Vetää seuraavaan lukemattomaan kanavaan"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Vedä seuraava aihe"; +"Settings.GalleryCamera" = "Camera in Gallery"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Button"; +"Settings.SnapDeletionEffect" = "Message Deletion Effects"; + +"Settings.Stickers.Size" = "SIZE"; +"Settings.Stickers.Timestamp" = "Show Timestamp"; + +"Settings.RecordingButton" = "Voice Recording Button"; + +"Settings.DefaultEmojisFirst" = "Anna etusijalle vakiotunnuksia"; +"Settings.DefaultEmojisFirst.Notice" = "Näytä vakiotunnukset ennen premium-tunnuksia tunnusnäppäimistössä"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "created: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Joined %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Rekisteröity"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Paina kahdesti muokataksesi viestiä"; + +"Settings.wideChannelPosts" = "Leveitä viestejä kanavissa"; +"Settings.ForceEmojiTab" = "Emoji-näppäimistö oletuksena"; + +"Settings.forceBuiltInMic" = "Pakota laitteen mikrofoni"; +"Settings.forceBuiltInMic.Notice" = "Jos otettu käyttöön, sovellus käyttää vain laitteen mikrofonia, vaikka kuulokkeet olisivatkin liitettynä."; + +"Settings.hideChannelBottomButton" = "Piilota kanavan alapalkki"; diff --git a/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..adc9a1b3d3b --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Paramètres du contenu"; + +"Settings.Tabs.Header" = "ONGLETS"; +"Settings.Tabs.HideTabBar" = "Masquer la barre d'onglets"; +"Settings.Tabs.ShowContacts" = "Afficher l'onglet Contacts"; +"Settings.Tabs.ShowNames" = "Afficher les noms des onglets"; + +"Settings.Folders.BottomTab" = "Dossiers en bas"; +"Settings.Folders.BottomTabStyle" = "Style des dossiers inférieurs"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Masquer \"%@\""; +"Settings.Folders.RememberLast" = "Ouvrir le dernier dossier"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram ouvrira le dernier dossier utilisé après le redémarrage ou changement de compte"; + +"Settings.Folders.CompactNames" = "Espacement plus petit"; +"Settings.Folders.AllChatsTitle" = "Titre \"Tous les Chats\""; +"Settings.Folders.AllChatsTitle.short" = "Courte"; +"Settings.Folders.AllChatsTitle.long" = "Longue"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Par défaut"; + + +"Settings.ChatList.Header" = "LISTE DE CHAT"; +"Settings.CompactChatList" = "Liste de discussion compacte"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Cacher les histoires"; +"Settings.Stories.WarnBeforeView" = "Demander avant de visionner"; +"Settings.Stories.DisableSwipeToRecord" = "Désactiver le glissement pour enregistrer"; + +"Settings.Translation.QuickTranslateButton" = "Bouton de traduction rapide"; + +"Stories.Warning.Author" = "Auteur"; +"Stories.Warning.ViewStory" = "Voir l'histoire?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SERA autorisé à voir que vous avez vu son histoire."; +"Stories.Warning.NoticeStealth" = "%@ ne sera pas en mesure de voir que vous avez vu leur Histoire."; + +"Settings.Photo.Quality.Notice" = "Qualité des photos et des récits photo sortants"; +"Settings.Photo.SendLarge" = "Envoyer de grandes photos"; +"Settings.Photo.SendLarge.Notice" = "Augmenter la limite latérale des images compressées à 2560px"; + +"Settings.VideoNotes.Header" = "VIDÉOS RONDES"; +"Settings.VideoNotes.StartWithRearCam" = "Commencer avec la caméra arrière"; + +"Settings.CustomColors.Header" = "COULEURS DU COMPTE"; +"Settings.CustomColors.Saturation" = "SATURATION"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Régler la saturation à 0%% pour désactiver les couleurs du compte"; + +"Settings.UploadsBoost" = "Chargements boost"; +"Settings.DownloadsBoost" = "Boost de téléchargements"; +"Settings.DownloadsBoost.none" = "Désactivé"; +"Settings.DownloadsBoost.medium" = "Moyenne"; +"Settings.DownloadsBoost.maximum" = "Maximum"; + +"Settings.ShowProfileID" = "Afficher l'identifiant du profil"; +"Settings.ShowDC" = "Afficher le centre de données"; +"Settings.ShowCreationDate" = "Afficher la date de création du chat"; +"Settings.ShowCreationDate.Notice" = "La date de création peut être inconnue pour certains chats."; + +"Settings.ShowRegDate" = "Afficher la date d'inscription"; +"Settings.ShowRegDate.Notice" = "La date d'inscription est approximative."; + +"Settings.SendWithReturnKey" = "Envoyer avec la clé \"return\""; +"Settings.HidePhoneInSettingsUI" = "Masquer le téléphone dans les paramètres"; +"Settings.HidePhoneInSettingsUI.Notice" = "Votre numéro sera masqué dans l'interface utilisateur uniquement. Pour le masquer aux autres, veuillez utiliser les paramètres de confidentialité."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Si absente pendant 5 secondes"; + +"ProxySettings.UseSystemDNS" = "Utiliser le DNS du système"; +"ProxySettings.UseSystemDNS.Notice" = "Utiliser le DNS système pour contourner le délai d'attente si vous n'avez pas accès à Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Vous **n'avez pas besoin** %@!"; +"Common.RestartRequired" = "Redémarrage nécessaire"; +"Common.RestartNow" = "Redémarrer maintenant"; +"Common.OpenTelegram" = "Ouvrir Telegram"; +"Common.UseTelegramForPremium" = "Veuillez noter que pour obtenir Telegram Premium, vous devez utiliser l'application Telegram officielle. Une fois que vous avez obtenu Telegram Premium, toutes ses fonctionnalités seront disponibles dans Swiftgram."; + +"Message.HoldToShowOrReport" = "Maintenir pour afficher ou rapporter."; + +"Auth.AccountBackupReminder" = "Assurez-vous d'avoir une méthode d'accès de sauvegarde. Gardez une carte SIM pour les SMS ou une session supplémentaire connectée pour éviter d'être bloquée."; +"Auth.UnofficialAppCodeTitle" = "Vous ne pouvez obtenir le code qu'avec l'application officielle"; + +"Settings.SmallReactions" = "Petites réactions"; +"Settings.HideReactions" = "Masquer les réactions"; + +"ContextMenu.SaveToCloud" = "Sauvegarder dans le cloud"; +"ContextMenu.SelectFromUser" = "Sélectionner de l'Auteur"; + +"Settings.ContextMenu" = "MENU CONTEXTUEL"; +"Settings.ContextMenu.Notice" = "Les entrées désactivées seront disponibles dans le sous-menu 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "Options de balayage de la liste de chat"; +"Settings.DeleteChatSwipeOption" = "Glisser pour supprimer la conversation"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Tirer vers le prochain canal non lu"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Tirer pour le sujet suivant"; +"Settings.GalleryCamera" = "Appareil photo dans la galerie"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Bouton \"%@\""; +"Settings.SnapDeletionEffect" = "Effets de suppression de message"; + +"Settings.Stickers.Size" = "TAILLE"; +"Settings.Stickers.Timestamp" = "Afficher l'horodatage"; + +"Settings.RecordingButton" = "Bouton d'enregistrement vocal"; + +"Settings.DefaultEmojisFirst" = "Prioriser les emojis standard"; +"Settings.DefaultEmojisFirst.Notice" = "Afficher les emojis standard avant les emojis premium dans le clavier emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "créé: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Rejoint %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Enregistré"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Appuyez deux fois pour modifier le message"; + +"Settings.wideChannelPosts" = "Messages larges dans les canaux"; +"Settings.ForceEmojiTab" = "Clavier emoji par défaut"; + +"Settings.forceBuiltInMic" = "Forcer le microphone de l'appareil"; +"Settings.forceBuiltInMic.Notice" = "Si activé, l'application utilisera uniquement le microphone de l'appareil même si des écouteurs sont connectés."; + +"Settings.hideChannelBottomButton" = "Masquer le panneau inférieur du canal"; diff --git a/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..563ba54e836 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "הגדרות תוכן"; + +"Settings.Tabs.Header" = "כרטיסיות"; +"Settings.Tabs.HideTabBar" = "הסתר סרגל לשוניים"; +"Settings.Tabs.ShowContacts" = "הצג כרטיסיית אנשי קשר"; +"Settings.Tabs.ShowNames" = "הצג שמות כרטיסיות"; + +"Settings.Folders.BottomTab" = "תיקיות בתחתית"; +"Settings.Folders.BottomTabStyle" = "סגנון תיקיות תחתון"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "טלגרם"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "להסתיר \"%@\""; +"Settings.Folders.RememberLast" = "פתח את התיקיה האחרונה"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram תפתח את התיקיה שנעשה בה שימוש לאחרונה לאחר הפעלה מחדש או החלפת חשבון"; + +"Settings.Folders.CompactNames" = "ריווח קטן יותר"; +"Settings.Folders.AllChatsTitle" = "כותרת \"כל הצ'אטים\""; +"Settings.Folders.AllChatsTitle.short" = "קצר"; +"Settings.Folders.AllChatsTitle.long" = "ארוך"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "ברירת מחדל"; + + +"Settings.ChatList.Header" = "רשימת צ'אטים"; +"Settings.CompactChatList" = "רשימת צ'אטים קומפקטית"; + +"Settings.Profiles.Header" = "פרופילים"; + +"Settings.Stories.Hide" = "הסתר סיפורים"; +"Settings.Stories.WarnBeforeView" = "שאל לפני צפייה"; +"Settings.Stories.DisableSwipeToRecord" = "בטל החלקה להקלטה"; + +"Settings.Translation.QuickTranslateButton" = "כפתור תרגום מהיר"; + +"Stories.Warning.Author" = "מחבר"; +"Stories.Warning.ViewStory" = "לצפות בסיפור?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ יוכל לראות שצפית בסיפור שלו."; +"Stories.Warning.NoticeStealth" = "%@ לא יוכל לראות שצפית בסיפור שלו."; + +"Settings.Photo.Quality.Notice" = "איכות התמונות היוצאות והסיפורים בתמונות"; +"Settings.Photo.SendLarge" = "שלח תמונות גדולות"; +"Settings.Photo.SendLarge.Notice" = "הגדל את הגבול הצידי של תמונות מודחקות ל-2560px"; + +"Settings.VideoNotes.Header" = "וידאו מעוגלים"; +"Settings.VideoNotes.StartWithRearCam" = "התחל עם מצלמה אחורית"; + +"Settings.CustomColors.Header" = "צבעי חשבון"; +"Settings.CustomColors.Saturation" = "רווי"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "קבע רווי ל-0%% כדי לבטל צבעי חשבון"; + +"Settings.UploadsBoost" = "תוספת העלאות"; +"Settings.DownloadsBoost" = "תוספת הורדות"; +"Settings.DownloadsBoost.none" = "מבוטל"; +"Settings.DownloadsBoost.medium" = "בינוני"; +"Settings.DownloadsBoost.maximum" = "מרבי"; + +"Settings.ShowProfileID" = "הצג מזהה פרופיל"; +"Settings.ShowDC" = "הצג מרכז מידע"; +"Settings.ShowCreationDate" = "הצג תאריך יצירת צ'אט"; +"Settings.ShowCreationDate.Notice" = "ייתכן שתאריך היצירה אינו ידוע עבור חלק מהצ'אטים."; + +"Settings.ShowRegDate" = "הצג תאריך רישום"; +"Settings.ShowRegDate.Notice" = "תאריך הרישום הוא אופציונלי."; + +"Settings.SendWithReturnKey" = "שלח עם מקש \"חזור\""; +"Settings.HidePhoneInSettingsUI" = "הסתר טלפון בהגדרות"; +"Settings.HidePhoneInSettingsUI.Notice" = "המספר שלך יהיה מוסתר בממשק ההגדרות בלבד. עבור להגדרות פרטיות כדי להסתיר אותו מאחרים."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "נעל אוטומטית אחרי 5 שניות"; + +"ProxySettings.UseSystemDNS" = "השתמש ב-DNS של המערכת"; +"ProxySettings.UseSystemDNS.Notice" = "השתמש ב-DNS של המערכת כדי לעקוף זמן תגובה אם אין לך גישה ל-Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "אין **צורך** ב%@!"; +"Common.RestartRequired" = "נדרש הפעלה מחדש"; +"Common.RestartNow" = "הפעל מחדש עכשיו"; +"Common.OpenTelegram" = "פתח טלגרם"; +"Common.UseTelegramForPremium" = "שים לב כי כדי לקבל Telegram Premium, עליך להשתמש באפליקציית Telegram הרשמית. לאחר שקיבלת טלגרם פרימיום, כל התכונות שלו יהיו זמינות ב־Swiftgram."; + +"Message.HoldToShowOrReport" = "החזק כדי להציג או לדווח."; + +"Auth.AccountBackupReminder" = "ודא שיש לך שיטת גישה לגיבוי. שמור כרטיס SIM ל-SMS או פתח סשן נוסף כדי למנוע חסימה."; +"Auth.UnofficialAppCodeTitle" = "תוכל לקבל את הקוד רק דרך האפליקציה הרשמית"; + +"Settings.SmallReactions" = "תגובות קטנות"; +"Settings.HideReactions" = "הסתר תגובות"; + +"ContextMenu.SaveToCloud" = "שמור בענן"; +"ContextMenu.SelectFromUser" = "בחר מהמשתמש"; + +"Settings.ContextMenu" = "תפריט הקשר"; +"Settings.ContextMenu.Notice" = "פריטים מבוטלים יהיו זמינים בתת-תפריט 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "אפשרויות גלילה ברשימת צ'אטים"; +"Settings.DeleteChatSwipeOption" = "החלק למחיקת הצ'אט"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "משוך לערוץ לא נקרא הבא"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "משוך כדי להמשיך לנושא הבא"; +"Settings.GalleryCamera" = "מצלמה בגלריה"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "כפתור \"%@\""; +"Settings.SnapDeletionEffect" = "אפקטים של מחיקת הודעות"; + +"Settings.Stickers.Size" = "גודל"; +"Settings.Stickers.Timestamp" = "הצג חותמת זמן"; + +"Settings.RecordingButton" = "כפתור הקלטת קול"; + +"Settings.DefaultEmojisFirst" = "העדף רמזי פנים סטנדרטיים"; +"Settings.DefaultEmojisFirst.Notice" = "הצג רמזי פנים סטנדרטיים לפני פרימיום במקלדת רמזי פנים"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "נוצר: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "הצטרף/הצטרפה ב־%@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "נרשם"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "לחץ פעמיים לעריכת הודעה"; + +"Settings.wideChannelPosts" = "פוסטים רחבים בערוצים"; +"Settings.ForceEmojiTab" = "מקלדת Emoji כברירת מחדל"; + +"Settings.forceBuiltInMic" = "כוח מיקרופון המכשיר"; +"Settings.forceBuiltInMic.Notice" = "אם מופעל, האפליקציה תשתמש רק במיקרופון המכשיר גם כאשר אוזניות מחוברות."; + +"Settings.hideChannelBottomButton" = "הסתר פאנל תחתון של ערוץ"; diff --git a/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..4481173f3e4 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "कंटेंट सेटिंग्स"; + +"Settings.Tabs.Header" = "टैब"; +"Settings.Tabs.HideTabBar" = "टैब बार छिपाएं"; +"Settings.Tabs.ShowContacts" = "संपर्क टैब दिखाएँ"; +"Settings.Tabs.ShowNames" = "टैब नाम दिखाएं"; + +"Settings.Folders.BottomTab" = "निचले टैब में फोल्डर्स"; +"Settings.Folders.BottomTabStyle" = "बॉटम फोल्डर स्टाइल है"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "आईओएस"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "टेलीग्राम"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "\"%@\" छिपाएं"; +"Settings.Folders.RememberLast" = "आखिरी फोल्डर खोलें"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram पुनः आरंभ या खाता स्विच करने के बाद अंतिम प्रयुक्त फोल्डर को खोलेगा"; + +"Settings.Folders.CompactNames" = "कम अंतराल"; +"Settings.Folders.AllChatsTitle" = "\"सभी चैट\" शीर्षक"; +"Settings.Folders.AllChatsTitle.short" = "संक्षिप्त"; +"Settings.Folders.AllChatsTitle.long" = "लंबा"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "डिफ़ॉल्ट"; + + +"Settings.ChatList.Header" = "चैट सूची"; +"Settings.CompactChatList" = "संक्षिप्त चैट सूची"; + +"Settings.Profiles.Header" = "प्रोफाइल"; + +"Settings.Stories.Hide" = "कहानियाँ छुपाएं"; +"Settings.Stories.WarnBeforeView" = "देखने से पहले पूछें"; +"Settings.Stories.DisableSwipeToRecord" = "रिकॉर्ड करने के लिए स्वाइप को अक्षम करें"; + +"Settings.Translation.QuickTranslateButton" = "त्वरित अनुवाद बटन"; + +"Stories.Warning.Author" = "लेखक"; +"Stories.Warning.ViewStory" = "कहानी देखें"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ देख सकते हैं कि आपने उनकी कहानी देखी है।"; +"Stories.Warning.NoticeStealth" = "%@ नहीं देख सकते कि आपने उनकी कहानी देखी है।"; + +"Settings.Photo.Quality.Notice" = "भेजे गए फोटो और फोटो-कहानियों की गुणवत्ता"; +"Settings.Photo.SendLarge" = "बड़े फोटो भेजें"; +"Settings.Photo.SendLarge.Notice" = "संकुचित छवियों पर साइड सीमा को 2560px तक बढ़ाएं"; + +"Settings.VideoNotes.Header" = "गोल वीडियो"; +"Settings.VideoNotes.StartWithRearCam" = "रियर कैमरा के साथ शुरू करें"; + +"Settings.CustomColors.Header" = "खाता रंग"; +"Settings.CustomColors.Saturation" = "संतृप्ति"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "खाता रंगों को निष्क्रिय करने के लिए संतृप्ति को 0%% पर सेट करें"; + +"Settings.UploadsBoost" = "अपलोड बूस्ट"; +"Settings.DownloadsBoost" = "डाउनलोड बूस्ट"; +"Settings.DownloadsBoost.none" = "निष्क्रिय"; +"Settings.DownloadsBoost.medium" = "माध्यम"; +"Settings.DownloadsBoost.maximum" = "अधिकतम"; + +"Settings.ShowProfileID" = "प्रोफ़ाइल ID दिखाएं"; +"Settings.ShowDC" = "डेटा सेंटर दिखाएं"; +"Settings.ShowCreationDate" = "चैट निर्माण तिथि दिखाएं"; +"Settings.ShowCreationDate.Notice" = "कुछ चैट के लिए निर्माण तिथि अज्ञात हो सकती है।"; + +"Settings.ShowRegDate" = "पंजीकरण दिनांक दिखाएं"; +"Settings.ShowRegDate.Notice" = "पंजीकरण दिनांक अनुमानित हो सकती है।"; + +"Settings.SendWithReturnKey" = "\"वापसी\" कुंजी के साथ भेजें"; +"Settings.HidePhoneInSettingsUI" = "सेटिंग्स में फोन छिपाएं"; +"Settings.HidePhoneInSettingsUI.Notice" = "आपका नंबर केवल सेटिंग्स UI में छिपा होगा। इसे दूसरों से छिपाने के लिए गोपनीयता सेटिंग्स में जाएं।"; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "5 सेकंड के लिए दूर रहने पर"; + +"ProxySettings.UseSystemDNS" = "सिस्टम डीएनएस का प्रयोग करें"; +"ProxySettings.UseSystemDNS.Notice" = "यदि आपके पास Google DNS तक पहुँच नहीं है तो टाइमआउट से बचने के लिए सिस्टम DNS का उपयोग करें"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "आपको %@ की **आवश्यकता नहीं** है!"; +"Common.RestartRequired" = "पुनः आरंभ की आवश्यकता"; +"Common.RestartNow" = "अभी रीस्टार्ट करें"; +"Common.OpenTelegram" = "टेलीग्राम खोलें"; +"Common.UseTelegramForPremium" = "कृपया ध्यान दें कि टेलीग्राम प्रीमियम प्राप्त करने के लिए आपको आधिकारिक टेलीग्राम ऐप का उपयोग करना होगा। एक बार जब आप टेलीग्राम प्रीमियम प्राप्त कर लेंगे, तो इसकी सभी सुविधाएं स्विफ्टग्राम में उपलब्ध हो जाएंगी।"; + +"Message.HoldToShowOrReport" = "दिखाने या रिपोर्ट करने के लिए दबाए रखें।"; + +"Auth.AccountBackupReminder" = "सुनिश्चित करें कि आपके पास बैकअप एक्सेस विधि है। एसएमएस के लिए एक सिम रखें या बाहर निकलने से बचने के लिए एक अतिरिक्त सत्र में लॉग इन करें।"; +"Auth.UnofficialAppCodeTitle" = "आप केवल आधिकारिक ऐप से ही कोड प्राप्त कर सकते हैं"; + +"Settings.SmallReactions" = "छोटी-छोटी प्रतिक्रियाएँ"; +"Settings.HideReactions" = "प्रतिक्रियाएँ छिपाएं"; + +"ContextMenu.SaveToCloud" = "क्लाउड में सहेजें"; +"ContextMenu.SelectFromUser" = "लेखक में से चुनें"; + +"Settings.ContextMenu" = "संदर्भ मेनू"; +"Settings.ContextMenu.Notice" = "अक्षम प्रविष्टियाँ \"स्विफ्टग्राम\" उप-मेनू में उपलब्ध होंगी।"; + + +"Settings.ChatSwipeOptions" = "चैटलिस्ट स्वाइप विकल्प"; +"Settings.DeleteChatSwipeOption" = "चैट हटाने के लिए स्वैप करें"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "अगले अपठित चैनल पर खींचें"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "अगले विषय को खींचें"; +"Settings.GalleryCamera" = "गैलरी में कैमरा"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" बटन"; +"Settings.SnapDeletionEffect" = "संदेश विलोपन प्रभाव"; + +"Settings.Stickers.Size" = "आकार"; +"Settings.Stickers.Timestamp" = "टाइमस्टैंप दिखाएं"; + +"Settings.RecordingButton" = "वॉयस रिकॉर्डिंग बटन"; + +"Settings.DefaultEmojisFirst" = "मुख्यत: मानक इमोजी को प्राथमिकता दें"; +"Settings.DefaultEmojisFirst.Notice" = "इमोजी कीबोर्ड में प्रीमियम से पहले मानक इमोजी दिखाएं"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "बनाया गया: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "%@ में शामिल हो गया"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "पंजीकृत"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "संदेश संपादित करने के लिए दो बार टैप करें"; + +"Settings.wideChannelPosts" = "चैनल में चौड़े पोस्ट"; +"Settings.ForceEmojiTab" = "डिफ़ॉल्ट ईमोजी कुंजीपटल"; + +"Settings.forceBuiltInMic" = "फ़ोर्स डिवाइस माइक्रोफ़ोन"; +"Settings.forceBuiltInMic.Notice" = "यदि सक्षम है, ऐप केवल उपकरण का माइक्रोफ़ोन उपयोग करेगा भले ही हेडफ़ोन कनेक्ट किए हों।"; + +"Settings.hideChannelBottomButton" = "चैनल बॉटम पैनल छिपाएँ"; diff --git a/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..fe2d99c29f2 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Tartalombeállítások"; + +"Settings.Tabs.Header" = "FÜLEK"; +"Settings.Tabs.HideTabBar" = "Feliratcsík elrejtése"; +"Settings.Tabs.ShowContacts" = "Kapcsolatok fül megjelenítése"; +"Settings.Tabs.ShowNames" = "Feliratcsík nevek megjelenítése"; + +"Settings.Folders.BottomTab" = "Könyvtárak az alján"; +"Settings.Folders.BottomTabStyle" = "Alsó könyvtár stílus"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Elrejtése \"%@\""; +"Settings.Folders.RememberLast" = "Utolsó mappa megnyitása"; +"Settings.Folders.RememberLast.Notice" = "A Swiftgram az utoljára használt mappát fogja megnyitni, amikor újraindítja az alkalmazást vagy fiókok között vált."; + +"Settings.Folders.CompactNames" = "Kisebb térköz"; +"Settings.Folders.AllChatsTitle" = "\"Minden Beszélgetés\" cím"; +"Settings.Folders.AllChatsTitle.short" = "Rövid"; +"Settings.Folders.AllChatsTitle.long" = "Hosszú"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Alapértelmezett"; + + +"Settings.ChatList.Header" = "BESZÉLGETÉS LISTA"; +"Settings.CompactChatList" = "Kompakt Beszélgetés Lista"; + +"Settings.Profiles.Header" = "PROFIL"; + +"Settings.Stories.Hide" = "Történetek elrejtése"; +"Settings.Stories.WarnBeforeView" = "Kérdezzen megtekintés előtt"; +"Settings.Stories.DisableSwipeToRecord" = "Húzás letiltása felvételhez"; + +"Settings.Translation.QuickTranslateButton" = "Gyors Fordítás gomb"; + +"Stories.Warning.Author" = "Szerző"; +"Stories.Warning.ViewStory" = "Történet megtekintése?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ LÁTNI FOGJA, hogy megtekintetted a történetüket."; +"Stories.Warning.NoticeStealth" = "%@ nem fogja látni, hogy megtekintetted a történetüket."; + +"Settings.Photo.Quality.Notice" = "Feltöltött fényképek és történetek minősége."; +"Settings.Photo.SendLarge" = "Nagy fényképek küldése"; +"Settings.Photo.SendLarge.Notice" = "Növelje a tömörített képek oldalméretének határát 2560px-re."; + +"Settings.VideoNotes.Header" = "KEREK VIDEÓK"; +"Settings.VideoNotes.StartWithRearCam" = "Kezdje a hátsó kamerával"; + +"Settings.CustomColors.Header" = "FIÓK SZÍNEI"; +"Settings.CustomColors.Saturation" = "TELÍTETTSÉG"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Színértéket 0%%-ra állítva az fiókszíneket letiltja."; + +"Settings.UploadsBoost" = "Feltöltés fokozása"; +"Settings.DownloadsBoost" = "Letöltés fokozása"; +"Settings.DownloadsBoost.none" = "Kikapcsolva"; +"Settings.DownloadsBoost.medium" = "Közepes"; +"Settings.DownloadsBoost.maximum" = "Maximális"; + +"Settings.ShowProfileID" = "Profil azonosító megjelenítése"; +"Settings.ShowDC" = "Adatközpont megjelenítése"; +"Settings.ShowCreationDate" = "Beszélgetés létrehozásának dátumának megjelenítése"; +"Settings.ShowCreationDate.Notice" = "A beszélgetés létrehozásának dátuma ismeretlen lehet néhány csevegésnél."; + +"Settings.ShowRegDate" = "Regisztrációs Dátum Megjelenítése"; +"Settings.ShowRegDate.Notice" = "A regisztrációs dátum csak hozzávetőleges."; + +"Settings.SendWithReturnKey" = "Küldés 'vissza' gombbal"; +"Settings.HidePhoneInSettingsUI" = "Telefonszám elrejtése a beállításokban"; +"Settings.HidePhoneInSettingsUI.Notice" = "Ezzel csak a telefonszámát rejti el a beállítások felületen. Ha mások számára is el akarja rejteni, menjen a Adatvédelem és biztonság menübe."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Ha 5 másodpercig távol van"; + +"ProxySettings.UseSystemDNS" = "Rendszer DNS használata"; +"ProxySettings.UseSystemDNS.Notice" = "Használja a rendszer DNS-t, ha nem fér hozzá a Google DNS-hez"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Nem **szükséges** %@!"; +"Common.RestartRequired" = "Újraindítás szükséges"; +"Common.RestartNow" = "Újraindítás most"; +"Common.OpenTelegram" = "Telegram megnyitása"; +"Common.UseTelegramForPremium" = "Kérjük vegye figyelembe, hogy a Telegram Prémiumhoz az hivatalos Telegram appot kell használnia. Amint megkapta a Telegram Prémiumot, Swiftgram összes funkciója elérhető lesz."; + +"Message.HoldToShowOrReport" = "Tartsa lenyomva a Megjelenítéshez vagy Jelentéshez."; + +"Auth.AccountBackupReminder" = "Győződjön meg róla, hogy van biztonsági másolat hozzáférési módszere. Tartsa meg a SMS-hez használt SIM-et vagy egy másik bejelentkezett munkamenetet, hogy elkerülje a kizárást."; +"Auth.UnofficialAppCodeTitle" = "A kódot csak a hivatalos alkalmazással szerezheti meg"; + +"Settings.SmallReactions" = "Kis reakciók"; +"Settings.HideReactions" = "Reakciók Elrejtése"; + +"ContextMenu.SaveToCloud" = "Mentés a Felhőbe"; +"ContextMenu.SelectFromUser" = "Kiválasztás a Szerzőtől"; + +"Settings.ContextMenu" = "KONTEXTUS MENÜ"; +"Settings.ContextMenu.Notice" = "A kikapcsolt bejegyzések elérhetők lesznek a 'Swiftgram' almenüjében."; + + +"Settings.ChatSwipeOptions" = "Csevegőlista húzás opciók"; +"Settings.DeleteChatSwipeOption" = "Húzza át az üzenet törléséhez"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Húzza a következő olvasatlan csatornához"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Húzza le a következő témához"; +"Settings.GalleryCamera" = "Kamera a Galériában"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Gomb"; +"Settings.SnapDeletionEffect" = "Üzenet törlés hatások"; + +"Settings.Stickers.Size" = "MÉRET"; +"Settings.Stickers.Timestamp" = "Időbélyeg Megjelenítése"; + +"Settings.RecordingButton" = "Hangrögzítés Gomb"; + +"Settings.DefaultEmojisFirst" = "Prioritize standard emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Mutassa az alap emojisokat az emoji billentyűzet előtt a prémiumok helyett"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "létrehozva: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Csatlakozott %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Regisztrált"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dupla koppintás a üzenet szerkesztéséhez"; + +"Settings.wideChannelPosts" = "Széles posztok csatornákban"; +"Settings.ForceEmojiTab" = "Alapértelmezett Emoji billentyűzet"; + +"Settings.forceBuiltInMic" = "Eszköz mikrofonjának kényszerítése"; +"Settings.forceBuiltInMic.Notice" = "Ha engedélyezve van, az alkalmazás csak az eszköz mikrofonját fogja használni, még akkor is, ha a fejhallgató csatlakoztatva van."; + +"Settings.hideChannelBottomButton" = "Kanal Alsó Panel Elrejtése"; diff --git a/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..7a7a7940fde --- /dev/null +++ b/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Pengaturan Konten"; + +"Settings.Tabs.Header" = "TABS"; +"Settings.Tabs.HideTabBar" = "Sembunyikan Tab bar"; +"Settings.Tabs.ShowContacts" = "Tampilkan Tab Kontak"; +"Settings.Tabs.ShowNames" = "Tampilkan Nama Tab"; + +"Settings.Folders.BottomTab" = "Folder di bawah"; +"Settings.Folders.BottomTabStyle" = "Gaya folder bawah"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Sembunyikan \"%@\""; +"Settings.Folders.RememberLast" = "Buka folder terakhir"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram akan membuka folder yang terakhir digunakan setelah restart atau pergantian akun"; + +"Settings.Folders.CompactNames" = "Pemisahan yang Lebih Kecil"; +"Settings.Folders.AllChatsTitle" = "Judul \"Semua Obrolan\""; +"Settings.Folders.AllChatsTitle.short" = "Pendek"; +"Settings.Folders.AllChatsTitle.long" = "Panjang"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Default"; + + +"Settings.ChatList.Header" = "DAFTAR OBROLAN"; +"Settings.CompactChatList" = "Daftar Obrolan Kompak"; + +"Settings.Profiles.Header" = "PROFIL"; + +"Settings.Stories.Hide" = "Sembunyikan Cerita"; +"Settings.Stories.WarnBeforeView" = "Tanyakan sebelum melihat"; +"Settings.Stories.DisableSwipeToRecord" = "Nonaktifkan geser untuk merekam"; + +"Settings.Translation.QuickTranslateButton" = "Bottone di traduzione rapida"; + +"Stories.Warning.Author" = "Penulis"; +"Stories.Warning.ViewStory" = "Lihat Cerita?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ AKAN TAHU bahwa Anda telah melihat Cerita mereka."; +"Stories.Warning.NoticeStealth" = "%@ tidak akan tahu bahwa Anda telah melihat Cerita mereka."; + +"Settings.Photo.Quality.Notice" = "Kualitas foto keluar dan cerita foto"; +"Settings.Photo.SendLarge" = "Kirim foto berukuran besar"; +"Settings.Photo.SendLarge.Notice" = "Tingkatkan batas sisi pada gambar terkompresi menjadi 2560px"; + +"Settings.VideoNotes.Header" = "VIDEO BULAT"; +"Settings.VideoNotes.StartWithRearCam" = "Mulai dengan kamera belakang"; + +"Settings.CustomColors.Header" = "WARNA AKUN"; +"Settings.CustomColors.Saturation" = "SATURASI"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Setel saturasi menjadi 0%% untuk menonaktifkan warna akun"; + +"Settings.UploadsBoost" = "Peningkatan Unggahan"; +"Settings.DownloadsBoost" = "Peningkatan Unduhan"; +"Settings.DownloadsBoost.none" = "Nonaktif"; +"Settings.DownloadsBoost.medium" = "Sedang"; +"Settings.DownloadsBoost.maximum" = "Maksimal"; + +"Settings.ShowProfileID" = "Tampilkan ID Profil"; +"Settings.ShowDC" = "Tampilkan Pusat Data"; +"Settings.ShowCreationDate" = "Tampilkan Tanggal Pembuatan Obrolan"; +"Settings.ShowCreationDate.Notice" = "Tanggal pembuatan mungkin tidak diketahui untuk beberapa obrolan."; + +"Settings.ShowRegDate" = "Tampilkan Tanggal Pendaftaran"; +"Settings.ShowRegDate.Notice" = "Tanggal pendaftaran adalah perkiraan."; + +"Settings.SendWithReturnKey" = "Kirim dengan kunci \"kembali\""; +"Settings.HidePhoneInSettingsUI" = "Sembunyikan nomor telepon di pengaturan"; +"Settings.HidePhoneInSettingsUI.Notice" = "Nomor Anda akan disembunyikan hanya di UI Pengaturan. Kunjungi Pengaturan Privasi untuk menyembunyikannya dari orang lain."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Jika menjauh selama 5 detik"; + +"ProxySettings.UseSystemDNS" = "Gunakan DNS sistem"; +"ProxySettings.UseSystemDNS.Notice" = "Gunakan DNS sistem untuk menghindari timeout jika Anda tidak memiliki akses ke Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Anda **tidak memerlukan** %@!"; +"Common.RestartRequired" = "Diperlukan restart"; +"Common.RestartNow" = "Restart Sekarang"; +"Common.OpenTelegram" = "Buka Telegram"; +"Common.UseTelegramForPremium" = "Harap dicatat bahwa untuk mendapatkan Telegram Premium, Anda harus menggunakan aplikasi Telegram resmi. Setelah Anda mendapatkan Telegram Premium, semua fiturnya akan tersedia di Swiftgram."; + +"Message.HoldToShowOrReport" = "Tahan untuk Menampilkan atau Melaporkan."; + +"Auth.AccountBackupReminder" = "Pastikan Anda memiliki metode akses cadangan. Simpan SIM untuk SMS atau sesi tambahan yang masuk untuk menghindari terkunci."; +"Auth.UnofficialAppCodeTitle" = "Anda hanya dapat mendapatkan kode dengan aplikasi resmi"; + +"Settings.SmallReactions" = "Reaksi kecil"; +"Settings.HideReactions" = "Sembunyikan Reaksi"; + +"ContextMenu.SaveToCloud" = "Simpan ke Cloud"; +"ContextMenu.SelectFromUser" = "Pilih dari Penulis"; + +"Settings.ContextMenu" = "MENU KONTEKS"; +"Settings.ContextMenu.Notice" = "Entri yang dinonaktifkan akan tersedia di sub-menu \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Opsi gesek daftar obrolan"; +"Settings.DeleteChatSwipeOption" = "Geser untuk Menghapus Obrolan"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Tarik untuk obrolan berikutnya"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Tarik ke Topik Berikutnya"; +"Settings.GalleryCamera" = "Kamera di galeri"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Tombol \"%@\""; +"Settings.SnapDeletionEffect" = "Efek penghapusan pesan"; + +"Settings.Stickers.Size" = "UKURAN"; +"Settings.Stickers.Timestamp" = "Tampilkan Timestamp"; + +"Settings.RecordingButton" = "Tombol Perekaman Suara"; + +"Settings.DefaultEmojisFirst" = "Berikan prioritas pada emoji standar"; +"Settings.DefaultEmojisFirst.Notice" = "Tampilkan emoji standar sebelum emoji premium di papan tombol emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "dibuat: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Bergabung %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Terdaftar"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Ketuk dua kali untuk mengedit pesan"; + +"Settings.wideChannelPosts" = "Pos Luas di Saluran"; +"Settings.ForceEmojiTab" = "Papan emoji secara default"; + +"Settings.forceBuiltInMic" = "Paksa Mikrofon Perangkat"; +"Settings.forceBuiltInMic.Notice" = "Jika diaktifkan, aplikasi akan menggunakan hanya mikrofon perangkat bahkan jika headphone terhubung."; + +"Settings.hideChannelBottomButton" = "Sembunyikan Panel Bawah Saluran"; diff --git a/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..dd44eab825b --- /dev/null +++ b/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings @@ -0,0 +1,148 @@ +"Settings.ContentSettings" = "Impostazioni Contenuto"; + +"Settings.Tabs.Header" = "TAB"; +"Settings.Tabs.HideTabBar" = "Nascondi barra della tab"; +"Settings.Tabs.ShowContacts" = "Mostra tab contatti"; +"Settings.Tabs.ShowNames" = "Mostra nomi tab"; + +"Settings.Folders.BottomTab" = "Cartelle in basso"; +"Settings.Folders.BottomTabStyle" = "Stile cartelle in basso"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Swiftgram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Nascondi \"%@\""; +"Settings.Folders.RememberLast" = "Apri l'ultima cartella"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram aprirà l'ultima cartella utilizzata dopo il riavvio o il cambio account"; + +"Settings.Folders.CompactNames" = "Spaziatura minore"; +"Settings.Folders.AllChatsTitle" = "Titolo \"Tutte le chat\""; +"Settings.Folders.AllChatsTitle.short" = "Breve"; +"Settings.Folders.AllChatsTitle.long" = "Lungo"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Predefinito"; + + +"Settings.ChatList.Header" = "ELENCO CHAT"; +"Settings.CompactChatList" = "Lista chat compatta"; + +"Settings.Profiles.Header" = "PROFILI"; + +"Settings.Stories.Hide" = "Nascondi Storie"; +"Settings.Stories.WarnBeforeView" = "Chiedi prima di visualizzare"; +"Settings.Stories.DisableSwipeToRecord" = "Disabilita lo scorrimento per registrare"; + +"Settings.Translation.QuickTranslateButton" = "Pulsante traduzione rapida"; + +"Stories.Warning.Author" = "Autore"; +"Stories.Warning.ViewStory" = "Visualizzare la storia?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SAPRÀ CHE HAI VISTO la storia."; +"Stories.Warning.NoticeStealth" = "%@ non saprà che hai visto la storia."; + +"Settings.Photo.Quality.Notice" = "Qualità delle foto inviate e foto nelle storie"; +"Settings.Photo.SendLarge" = "Invia foto di grandi dimensioni"; +"Settings.Photo.SendLarge.Notice" = "Aumenta il limite sulla compressione delle foto a 2560px"; + +"Settings.VideoNotes.Header" = "Videomessaggi"; +"Settings.VideoNotes.StartWithRearCam" = "Inizia con la camera posteriore"; + +"Settings.CustomColors.Header" = "COLORI ACCOUNT"; +"Settings.CustomColors.Saturation" = "SATURAZIONE"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Imposta la saturazione a 0%% per disabilitare i colori dell'account"; + +"Settings.UploadsBoost" = "Potenziamento del caricamento"; +"Settings.DownloadsBoost" = "Potenziamento dello scaricamento"; +"Settings.DownloadsBoost.none" = "Disabilitato"; +"Settings.DownloadsBoost.medium" = "Intermedio"; +"Settings.DownloadsBoost.maximum" = "Massimo"; + +"Settings.ShowProfileID" = "Mostra l'ID del profilo"; +"Settings.ShowDC" = "Mostra Data Center"; +"Settings.ShowCreationDate" = "Mostra data di creazione della chat"; +"Settings.ShowCreationDate.Notice" = "La data di creazione potrebbe essere sconosciuta per alcune chat."; + +"Settings.ShowRegDate" = "Mostra data di registrazione"; +"Settings.ShowRegDate.Notice" = "La data di registrazione è approssimativa."; + +"Settings.SendWithReturnKey" = "Pulsante \"Invia\" per inviare"; +"Settings.HidePhoneInSettingsUI" = "Nascondi il numero di telefono nelle impostazioni"; +"Settings.HidePhoneInSettingsUI.Notice" = "Il tuo numero verrà nascosto solo nell'interfaccia. Per nasconderlo dagli altri, apri le impostazioni della Privacy."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Se assente per 5 secondi"; + +"ProxySettings.UseSystemDNS" = "Usa DNS di sistema"; +"ProxySettings.UseSystemDNS.Notice" = "Usa DNS di sistema per bypassare il timeout se non hai accesso al DNS di Google"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "**Non hai bisogno** di %@!"; +"Common.RestartRequired" = "Riavvio richiesto"; +"Common.RestartNow" = "Riavvia Adesso"; +"Common.OpenTelegram" = "Apri Telegram"; +"Common.UseTelegramForPremium" = "Si prega di notare che per ottenere Telegram Premium, è necessario utilizzare l'app ufficiale Telegram. Una volta ottenuto Telegram Premium, tutte le sue funzionalità saranno disponibili su Swiftgram."; + +"Message.HoldToShowOrReport" = "Tieni premuto per mostrare o segnalare."; + +"Auth.AccountBackupReminder" = "Assicurati di avere un metodo di accesso di backup. Tieni una SIM per gli SMS o delle sessioni aperte su altri dispositivi per evitare di essere bloccato fuori."; +"Auth.UnofficialAppCodeTitle" = "Puoi ottenere il codice solo con l'applicazione ufficiale"; + +"Settings.SmallReactions" = "Reazioni piccole"; +"Settings.HideReactions" = "Nascondi Reazioni"; + +"ContextMenu.SaveToCloud" = "Salva sul cloud"; +"ContextMenu.SelectFromUser" = "Seleziona dall'autore"; + +"Settings.ContextMenu" = "MENU CONTESTUALE"; +"Settings.ContextMenu.Notice" = "Le voci disabilitate saranno disponibili nel sottomenu \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Opzioni scorrimento nella lista delle chat"; +"Settings.DeleteChatSwipeOption" = "Swipe per eliminare chat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Tira per il prossimo canale non letto"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Scorri per il prossimo topic"; +"Settings.GalleryCamera" = "Fotocamera nella galleria"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Pulsante \"%@\""; +"Settings.SnapDeletionEffect" = "Effetti eliminazione messaggi"; + +"Settings.Stickers.Size" = "DIMENSIONE"; +"Settings.Stickers.Timestamp" = "Mostra timestamp"; + +"Settings.RecordingButton" = "Pulsante per la registrazione vocale"; + +"Settings.DefaultEmojisFirst" = "Dare priorità agli emoji standard"; +"Settings.DefaultEmojisFirst.Notice" = "Mostra gli emoji standard prima dei premium nella tastiera degli emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "creato il: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Sì è unito a %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrato"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Doppio tap per modificare il messaggio"; + +"Settings.wideChannelPosts" = "Ampie colonne nei canali"; +"Settings.ForceEmojiTab" = "Tastiera emoji predefinita"; + +"Settings.forceBuiltInMic" = "Forza Microfono Dispositivo"; +"Settings.forceBuiltInMic.Notice" = "Se abilitato, l'app utilizzerà solo il microfono del dispositivo anche se sono collegate le cuffie."; + +"Settings.hideChannelBottomButton" = "Nascondi Pannello Inferiore del Canale"; + +"Settings.CallConfirmation" = "Conferma di chiamata"; +"Settings.CallConfirmation.Notice" = "Swiftgram chiederà la tua conferma prima di effettuare una chiamata."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Effettuare una chiamata?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Effettuare una videochiamata?"; + +"MutualContact.Label" = "contatto reciproco"; diff --git a/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..72d5286c822 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings @@ -0,0 +1,138 @@ +"Settings.ContentSettings" = "コンテンツの設定"; + +"Settings.Tabs.Header" = "タブ"; +"Settings.Tabs.HideTabBar" = "タブバーを非表示にする"; +"Settings.Tabs.ShowContacts" = "連絡先のタブを表示"; +"Settings.Tabs.ShowNames" = "タブの名前を隠す"; + +"Settings.Folders.BottomTab" = "フォルダーを下に表示"; +"Settings.Folders.BottomTabStyle" = "チャットフォルダーのスタイル"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "\"%@\"を非表示"; +"Settings.Folders.RememberLast" = "最後に開いたフォルダを開く"; +"Settings.Folders.RememberLast.Notice" = "Swiftgramは再起動またはアカウント切替後に最後に使用したフォルダを開きます"; + +"Settings.Folders.CompactNames" = "より小さい間隔"; +"Settings.Folders.AllChatsTitle" = "「すべてのチャット」タイトル"; +"Settings.Folders.AllChatsTitle.short" = "Short"; +"Settings.Folders.AllChatsTitle.long" = "長い順"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "デフォルト"; + + +"Settings.ChatList.Header" = "チャットリスト"; +"Settings.CompactChatList" = "コンパクトなチャットリスト"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "ストーリーを隠す"; +"Settings.Stories.WarnBeforeView" = "視聴前に確認"; +"Settings.Stories.DisableSwipeToRecord" = "スワイプで録画を無効にする"; + +"Settings.Translation.QuickTranslateButton" = "クイック翻訳ボタン"; + +"Stories.Warning.Author" = "投稿者"; +"Stories.Warning.ViewStory" = "ストーリーを表示?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@はあなたがそのストーリーを見たことを確認できます。"; +"Stories.Warning.NoticeStealth" = "%@はあなたがそのストーリーを見たことを確認できません。"; + +"Settings.Photo.Quality.Notice" = "送信する写真とフォトストーリーの品質"; +"Settings.Photo.SendLarge" = "大きな写真を送信"; +"Settings.Photo.SendLarge.Notice" = "圧縮画像のサイド制限を2560pxに増加"; + +"Settings.VideoNotes.Header" = "丸いビデオ"; +"Settings.VideoNotes.StartWithRearCam" = "リアカメラで開始"; + +"Settings.CustomColors.Header" = "アカウントの色"; +"Settings.CustomColors.Saturation" = "彩度"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "彩度を0%%に設定してアカウントの色を無効にする"; + +"Settings.UploadsBoost" = "アップロードブースト"; +"Settings.DownloadsBoost" = "ダウンロードブースト"; +"Settings.DownloadsBoost.none" = "無効"; +"Settings.DownloadsBoost.medium" = "中程度"; +"Settings.DownloadsBoost.maximum" = "最大"; + +"Settings.ShowProfileID" = "プロフィールIDを表示"; +"Settings.ShowDC" = "データセンターを表示"; +"Settings.ShowCreationDate" = "チャットの作成日を表示"; +"Settings.ShowCreationDate.Notice" = "作成日が不明なチャットがあります。"; + +"Settings.ShowRegDate" = "登録日を表示"; +"Settings.ShowRegDate.Notice" = "登録日はおおよその日です。"; + +"Settings.SendWithReturnKey" = "\"return\" キーで送信"; +"Settings.HidePhoneInSettingsUI" = "設定で電話番号を隠す"; +"Settings.HidePhoneInSettingsUI.Notice" = "あなたの番号は設定UIでのみ隠されます。他の人から隠すにはプライバシー設定に移動してください。"; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "5秒間離れると自動ロック"; + +"ProxySettings.UseSystemDNS" = "システムDNSを使用"; +"ProxySettings.UseSystemDNS.Notice" = "Google DNSにアクセスできない場合はシステムDNSを使用してタイムアウトを回避"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "** %@は必要ありません**!"; +"Common.RestartRequired" = "再起動が必要です"; +"Common.RestartNow" = "今すぐ再実行"; +"Common.OpenTelegram" = "Telegram を開く"; +"Common.UseTelegramForPremium" = "Telegram Premiumを登録するには、公式のTelegramアプリが必要です。 +登録すると、Swiftgram等の非公式アプリ含め、Telegram Premiumをサポートする全てのアプリでプレミアムメソッドを利用できます。"; + +"Message.HoldToShowOrReport" = "表示または報告するために押し続ける。"; + +"Auth.AccountBackupReminder" = "バックアップアクセス方法があることを確認してください。SMS用のSIMを保持するか、追加のセッションにログインしてロックアウトを避けてください。"; +"Auth.UnofficialAppCodeTitle" = "テレグラムの公式アプリでのみログインコードを取得できます"; + +"Settings.SmallReactions" = "小さいリアクション"; +"Settings.HideReactions" = "リアクションを非表示"; + +"ContextMenu.SaveToCloud" = "メッセージを保存"; +"ContextMenu.SelectFromUser" = "全て選択"; + +"Settings.ContextMenu" = "コンテキスト メニュー"; +"Settings.ContextMenu.Notice" = "無効化されたエントリは、「Swiftgram」サブメニューから利用できます。"; + + +"Settings.ChatSwipeOptions" = "チャットリストのスワイプ設定"; +"Settings.DeleteChatSwipeOption" = "チャットを削除するにはスワイプしてください"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "次の未読チャンネルまでプルする"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "次のトピックに移動する"; +"Settings.GalleryCamera" = "ギャラリーのカメラを隠す"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" ボタン"; +"Settings.SnapDeletionEffect" = "メッセージ削除のエフェクト"; + +"Settings.Stickers.Size" = "サイズ"; +"Settings.Stickers.Timestamp" = "タイムスタンプを表示"; + +"Settings.RecordingButton" = "音声録音ボタン"; + +"Settings.DefaultEmojisFirst" = "標準エモジを優先"; +"Settings.DefaultEmojisFirst.Notice" = "絵文字キーボードでプレミアムより前に標準エモジを表示"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "作成済み: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "%@ に参加しました"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "登録済み"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "メッセージを編集するにはタップをダブルタップ"; + +"Settings.wideChannelPosts" = "チャンネル内の幅広い投稿"; +"Settings.ForceEmojiTab" = "デフォルトで絵文字キーボード"; + +"Settings.forceBuiltInMic" = "デバイスのマイクを強制"; +"Settings.forceBuiltInMic.Notice" = "有効にすると、ヘッドフォンが接続されていてもアプリはデバイスのマイクのみを使用します。"; + +"Settings.hideChannelBottomButton" = "チャンネルボトムパネルを非表示"; diff --git a/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..928cf393a67 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings @@ -0,0 +1,8 @@ +"Settings.Tabs.Header" = "ថេប"; +"Settings.Tabs.ShowContacts" = "បង្ហាញថេបទំនាក់ទំនង"; +"Settings.VideoNotes.Header" = "រង្វង់វីដេអូ"; +"Settings.VideoNotes.StartWithRearCam" = "ចាប់ផ្ដើមជាមួយកាមេរ៉ាក្រោយ"; +"Settings.Tabs.ShowNames" = "បង្ហាញឈ្មោះថេប"; +"Settings.HidePhoneInSettingsUI" = "លាក់លេខទូរសព្ទក្នុងការកំណត់"; +"Settings.Folders.BottomTab" = "ថតឯបាត"; +"ContextMenu.SaveToCloud" = "រក្សាទុកទៅពពក"; diff --git a/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..a4a1f0a050d --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "콘텐츠 설정"; + +"Settings.Tabs.Header" = "탭"; +"Settings.Tabs.HideTabBar" = "탭바숨기기"; +"Settings.Tabs.ShowContacts" = "연락처 탭 보이기"; +"Settings.Tabs.ShowNames" = "탭 이름 표시"; + +"Settings.Folders.BottomTab" = "폴더를 하단에 표시"; +"Settings.Folders.BottomTabStyle" = "탭위치아래"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "\"%@\" 숨기기"; +"Settings.Folders.RememberLast" = "마지막 폴더 열기"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram은 재시작하거나 계정을 전환한 후 마지막으로 사용한 폴더를 엽니다"; + +"Settings.Folders.CompactNames" = "간격 작게"; +"Settings.Folders.AllChatsTitle" = "\"모든 채팅\" 제목"; +"Settings.Folders.AllChatsTitle.short" = "단축"; +"Settings.Folders.AllChatsTitle.long" = "긴"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "기본"; + + +"Settings.ChatList.Header" = "채팅 목록"; +"Settings.CompactChatList" = "간략한 채팅 목록"; + +"Settings.Profiles.Header" = "프로필"; + +"Settings.Stories.Hide" = "스토리 숨기기"; +"Settings.Stories.WarnBeforeView" = "보기 전에 묻기"; +"Settings.Stories.DisableSwipeToRecord" = "녹화를 위한 스와이프 비활성화"; + +"Settings.Translation.QuickTranslateButton" = "빠른 번역 버튼"; + +"Stories.Warning.Author" = "작성자"; +"Stories.Warning.ViewStory" = "스토리 보기?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@님은 당신이 그들의 스토리를 봤는지 알 수 있습니다."; +"Stories.Warning.NoticeStealth" = "%@님은 당신이 그들의 스토리를 봤는지 알 수 없습니다."; + +"Settings.Photo.Quality.Notice" = "보낸 사진과 포토스토리의 품질"; +"Settings.Photo.SendLarge" = "큰 사진 보내기"; +"Settings.Photo.SendLarge.Notice" = "압축 이미지의 크기 제한을 2560px로 증가"; + +"Settings.VideoNotes.Header" = "라운드 비디오"; +"Settings.VideoNotes.StartWithRearCam" = "후면 카메라로 시작"; + +"Settings.CustomColors.Header" = "계정 색상"; +"Settings.CustomColors.Saturation" = "채도"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "계정 색상을 비활성화하려면 채도를 0%%로 설정하세요"; + +"Settings.UploadsBoost" = "업로드 향상"; +"Settings.DownloadsBoost" = "다운로드 향상"; +"Settings.DownloadsBoost.none" = "비활성화"; +"Settings.DownloadsBoost.medium" = "중간"; +"Settings.DownloadsBoost.maximum" = "최대"; + +"Settings.ShowProfileID" = "프로필 ID 표시"; +"Settings.ShowDC" = "데이터센터보기"; +"Settings.ShowCreationDate" = "채팅 생성 날짜 표시"; +"Settings.ShowCreationDate.Notice" = "몇몇 채팅에 대해서는 생성 날짜를 알 수 없을 수 있습니다."; + +"Settings.ShowRegDate" = "가입 날짜 표시"; +"Settings.ShowRegDate.Notice" = "가입 날짜는 대략적입니다."; + +"Settings.SendWithReturnKey" = "\"리턴\" 키로 보내기"; +"Settings.HidePhoneInSettingsUI" = "설정에서 전화번호 숨기기"; +"Settings.HidePhoneInSettingsUI.Notice" = "전화 번호는 UI에서만 숨겨집니다. 다른 사람에게 숨기려면 개인 정보 설정을 사용하세요."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "5초 동안 떨어져 있으면"; + +"ProxySettings.UseSystemDNS" = "시스템 DNS 사용"; +"ProxySettings.UseSystemDNS.Notice" = "Google DNS에 접근할 수 없는 경우 시스템 DNS를 사용하여 타임아웃 우회"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "%@이(가) **필요하지 않습니다**!"; +"Common.RestartRequired" = "재시작 필요"; +"Common.RestartNow" = "지금 재시작"; +"Common.OpenTelegram" = "텔레그램 열기"; +"Common.UseTelegramForPremium" = "텔레그램 프리미엄을 받으려면 공식 텔레그램 앱을 사용해야 합니다. 텔레그램 프리미엄을 획득하면 모든 기능이 Swiftgram에서 사용 가능해집니다."; + +"Message.HoldToShowOrReport" = "보여주거나 신고하기 위해 길게 누르세요."; + +"Auth.AccountBackupReminder" = "백업 접근 방법을 확보하세요. SMS용 SIM 카드를 보관하거나 추가 세션에 로그인하여 잠금을 피하세요."; +"Auth.UnofficialAppCodeTitle" = "코드는 공식 앱으로만 받을 수 있습니다"; + +"Settings.SmallReactions" = "작은 반응들"; +"Settings.HideReactions" = "반응 숨기기"; + +"ContextMenu.SaveToCloud" = "클라우드에 저장"; +"ContextMenu.SelectFromUser" = "사용자에서 선택"; + +"Settings.ContextMenu" = "컨텍스트 메뉴"; +"Settings.ContextMenu.Notice" = "'Swiftgram' 하위 메뉴에서 비활성화된 항목을 사용할 수 있습니다."; + + +"Settings.ChatSwipeOptions" = "채팅 목록 스와이프 옵션"; +"Settings.DeleteChatSwipeOption" = "채팅 삭제를 위해 스와이프하세요"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "다음 읽지 않은 채널까지 당겨서 보기"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "다음 주제로 끌어당기기"; +"Settings.GalleryCamera" = "갤러리 내 카메라"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" 버튼"; +"Settings.SnapDeletionEffect" = "메시지 삭제 효과"; + +"Settings.Stickers.Size" = "크기"; +"Settings.Stickers.Timestamp" = "시간 표시 표시"; + +"Settings.RecordingButton" = "음성 녹음 버튼"; + +"Settings.DefaultEmojisFirst" = "표준 이모지 우선순위 설정"; +"Settings.DefaultEmojisFirst.Notice" = "이모지 키보드에서 프리미엄 이모지보다 표준 이모지 우선 표시"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "생성됨: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "%@에 가입함"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "가입함"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "메시지 수정을 위해 두 번 탭"; + +"Settings.wideChannelPosts" = "채널의 넓은 게시물"; +"Settings.ForceEmojiTab" = "기본으로 이모티콘 키보드"; + +"Settings.forceBuiltInMic" = "장치 마이크 강제"; +"Settings.forceBuiltInMic.Notice" = "만약 활성화되면, 앱은 헤드폰이 연결되어 있더라도 장치 마이크만 사용합니다."; + +"Settings.hideChannelBottomButton" = "채널 하단 패널 숨기기"; diff --git a/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..62ac20a89c4 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings @@ -0,0 +1,10 @@ +"Settings.Tabs.Header" = "تابەکان"; +"Settings.Tabs.ShowContacts" = "نیشاندانی تابی کۆنتاکتەکان"; +"Settings.VideoNotes.Header" = "ڤیدیۆ بازنەییەکان"; +"Settings.VideoNotes.StartWithRearCam" = "دەستپێکردن بە کامێرای پشتەوە"; +"Settings.Tabs.ShowNames" = "نیشاندانی ناوی تابەکان"; +"Settings.HidePhoneInSettingsUI" = "شاردنەوەی تەلەفۆن لە ڕێکخستنەکان"; +"Settings.HidePhoneInSettingsUI.Notice" = "ژمارەکەت تەنها لە ڕووکارەکە دەرناکەوێت. بۆ ئەوەی لە ئەوانەی دیکەی بشاریتەوە، تکایە ڕێکخستنەکانی پارێزراوی بەکاربێنە."; +"Settings.Translation.QuickTranslateButton" = "دوگمەی وەرگێڕانی خێرا"; +"Settings.Folders.BottomTab" = "بوخچەکان لە خوارەوە"; +"ContextMenu.SaveToCloud" = "هەڵگرتن لە کڵاود"; diff --git a/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..4d2faf132d8 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Inhoudsinstellingen"; + +"Settings.Tabs.Header" = "TABS"; +"Settings.Tabs.HideTabBar" = "Tabbladbalk verbergen"; +"Settings.Tabs.ShowContacts" = "Toon Contacten Tab"; +"Settings.Tabs.ShowNames" = "Show Tab Names"; + +"Settings.Folders.BottomTab" = "Mappen onderaan"; +"Settings.Folders.BottomTabStyle" = "Onderste mappenstijl"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Verberg \"%@\""; +"Settings.Folders.RememberLast" = "Laatste map openen"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram zal de laatst gebruikte map openen wanneer u de app herstart of van account wisselt."; + +"Settings.Folders.CompactNames" = "Kleinere afstand"; +"Settings.Folders.AllChatsTitle" = "\"Alle Chats\" titel"; +"Settings.Folders.AllChatsTitle.short" = "Kort"; +"Settings.Folders.AllChatsTitle.long" = "Lang"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standaard"; + + +"Settings.ChatList.Header" = "CHAT LIJST"; +"Settings.CompactChatList" = "Compacte Chat Lijst"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Verberg Verhalen"; +"Settings.Stories.WarnBeforeView" = "Vragen voor bekijken"; +"Settings.Stories.DisableSwipeToRecord" = "Swipe om op te nemen uitschakelen"; + +"Settings.Translation.QuickTranslateButton" = "Snelle Vertaalknop"; + +"Stories.Warning.Author" = "Auteur"; +"Stories.Warning.ViewStory" = "Bekijk Verhaal?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ ZAL KUNNEN ZIEN dat je hun Verhaal hebt bekeken."; +"Stories.Warning.NoticeStealth" = "%@ zal niet kunnen zien dat je hun Verhaal hebt bekeken."; + +"Settings.Photo.Quality.Notice" = "Kwaliteit van geüploade foto's en verhalen."; +"Settings.Photo.SendLarge" = "Verstuur grote foto's"; +"Settings.Photo.SendLarge.Notice" = "Verhoog de zijlimiet bij gecomprimeerde afbeeldingen naar 2560px."; + +"Settings.VideoNotes.Header" = "RONDE VIDEO'S"; +"Settings.VideoNotes.StartWithRearCam" = "Start met achtercamera"; + +"Settings.CustomColors.Header" = "ACCOUNTKLEUREN"; +"Settings.CustomColors.Saturation" = "VERZADIGING"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Zet op 0%% om accountkleuren uit te schakelen."; + +"Settings.UploadsBoost" = "Upload Boost"; +"Settings.DownloadsBoost" = "Download Boost"; +"Settings.DownloadsBoost.none" = "Uitgeschakeld"; +"Settings.DownloadsBoost.medium" = "Gemiddeld"; +"Settings.DownloadsBoost.maximum" = "Maximaal"; + +"Settings.ShowProfileID" = "Toon profiel ID"; +"Settings.ShowDC" = "Toon datacentrum"; +"Settings.ShowCreationDate" = "Toon Chat Aanmaakdatum"; +"Settings.ShowCreationDate.Notice" = "De aanmaakdatum kan onbekend zijn voor sommige chatten."; + +"Settings.ShowRegDate" = "Toon registratiedatum"; +"Settings.ShowRegDate.Notice" = "De registratiedatum is ongeveer hetzelfde."; + +"Settings.SendWithReturnKey" = "Verstuur met 'return'-toets"; +"Settings.HidePhoneInSettingsUI" = "Verberg telefoon in Instellingen"; +"Settings.HidePhoneInSettingsUI.Notice" = "Dit verbergt alleen je telefoonnummer in de instellingen interface. Ga naar Privacy en Beveiliging om het voor anderen te verbergen."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Automatisch vergrendelen na 5 seconden"; + +"ProxySettings.UseSystemDNS" = "Gebruik systeem DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Gebruik systeem DNS om time-out te omzeilen als je geen toegang hebt tot Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Je hebt **geen %@ nodig**!"; +"Common.RestartRequired" = "Herstart vereist"; +"Common.RestartNow" = "Nu herstarten"; +"Common.OpenTelegram" = "Open Telegram"; +"Common.UseTelegramForPremium" = "Om Telegram Premium te krijgen moet je de officiële Telegram app gebruiken. Zodra je Telegram Premium hebt ontvangen, zullen alle functies ervan beschikbaar komen in Swiftgram."; + +"Message.HoldToShowOrReport" = "Houd vast om te Tonen of te Rapporteren."; + +"Auth.AccountBackupReminder" = "Zorg ervoor dat je een back-up toegangsmethode hebt. Houd een SIM voor SMS of een extra sessie ingelogd om buitensluiting te voorkomen."; +"Auth.UnofficialAppCodeTitle" = "Je kunt de code alleen krijgen met de officiële app"; + +"Settings.SmallReactions" = "Kleine reacties"; +"Settings.HideReactions" = "Verberg Reacties"; + +"ContextMenu.SaveToCloud" = "Opslaan in de Cloud"; +"ContextMenu.SelectFromUser" = "Selecteer van Auteur"; + +"Settings.ContextMenu" = "CONTEXTMENU"; +"Settings.ContextMenu.Notice" = "Uitgeschakelde items zijn beschikbaar in het 'Swiftgram'-submenu."; + + +"Settings.ChatSwipeOptions" = "Veegopties voor chatlijst"; +"Settings.DeleteChatSwipeOption" = "Veeg om Chat te Verwijderen"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Trek naar het volgende ongelezen kanaal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Trek naar Volgend Onderwerp"; +"Settings.GalleryCamera" = "Camera in Galerij"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" knop"; +"Settings.SnapDeletionEffect" = "Verwijderde Berichten Effecten"; + +"Settings.Stickers.Size" = "GROOTTE"; +"Settings.Stickers.Timestamp" = "Tijdstempel weergeven"; + +"Settings.RecordingButton" = "Spraakopname knop"; + +"Settings.DefaultEmojisFirst" = "Standaardemoji's prioriteren"; +"Settings.DefaultEmojisFirst.Notice" = "Toon standaardemoji's vóór premium in emoji-toetsenbord"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "aangemaakt: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Lid geworden %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Geregistreerd"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dubbelklik om bericht te bewerken"; + +"Settings.wideChannelPosts" = "Brede berichten in kanalen"; +"Settings.ForceEmojiTab" = "Emoji-toetsenbord standaard"; + +"Settings.forceBuiltInMic" = "Forceer Apparaatmicrofoon"; +"Settings.forceBuiltInMic.Notice" = "Indien ingeschakeld, zal de app alleen de apparaatmicrofoon gebruiken, zelfs als er hoofdtelefoons zijn aangesloten."; + +"Settings.hideChannelBottomButton" = "Verberg Kanaal Onderste Paneel"; diff --git a/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..93bee9db4ed --- /dev/null +++ b/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Innholdsinnstillinger"; + +"Settings.Tabs.Header" = "FANER"; +"Settings.Tabs.HideTabBar" = "Skjul fanelinjen"; +"Settings.Tabs.ShowContacts" = "Vis kontakter-fane"; +"Settings.Tabs.ShowNames" = "Show Tab Names"; + +"Settings.Folders.BottomTab" = "Mapper på bunnen"; +"Settings.Folders.BottomTabStyle" = "Stil for nedre mapper"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Skjul \"%@\""; +"Settings.Folders.RememberLast" = "Åpne siste mappe"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram vil åpne den sist brukte mappen når du starter appen på nytt eller bytter kontoer."; + +"Settings.Folders.CompactNames" = "Mindre avstand"; +"Settings.Folders.AllChatsTitle" = "\"Alle chater\" tittel"; +"Settings.Folders.AllChatsTitle.short" = "Kort"; +"Settings.Folders.AllChatsTitle.long" = "Lang"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standard"; + + +"Settings.ChatList.Header" = "CHAT LIST"; +"Settings.CompactChatList" = "Kompakt liste"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Skjul Historier"; +"Settings.Stories.WarnBeforeView" = "Spør før visning"; +"Settings.Stories.DisableSwipeToRecord" = "Deaktiver sveip for å ta opp"; + +"Settings.Translation.QuickTranslateButton" = "Hurtigoversettelsesknapp"; + +"Stories.Warning.Author" = "Forfatter"; +"Stories.Warning.ViewStory" = "Se Historie?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ VIL SE at du har sett deres Historie."; +"Stories.Warning.NoticeStealth" = "%@ vil ikke kunne se at du har sett deres Historie."; + +"Settings.Photo.Quality.Notice" = "Kvalitet på opplastede bilder og historier."; +"Settings.Photo.SendLarge" = "Send store bilder"; +"Settings.Photo.SendLarge.Notice" = "Øk grensen for komprimerte bilder til 2560 piksler."; + +"Settings.VideoNotes.Header" = "RUNDE VIDEOER"; +"Settings.VideoNotes.StartWithRearCam" = "Start med bakkamera"; + +"Settings.CustomColors.Header" = "KONTOFARGER"; +"Settings.CustomColors.Saturation" = "METNING"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Satt til 0%% for å deaktivere kontofarger."; + +"Settings.UploadsBoost" = "Ã k opplastingshastighet"; +"Settings.DownloadsBoost" = "Last ned boost"; +"Settings.DownloadsBoost.none" = "Deaktivert"; +"Settings.DownloadsBoost.medium" = "Middels"; +"Settings.DownloadsBoost.maximum" = "Maksimum"; + +"Settings.ShowProfileID" = "Vis profil-ID"; +"Settings.ShowDC" = "Vis datasenter"; +"Settings.ShowCreationDate" = "Vis chat opprettet dato"; +"Settings.ShowCreationDate.Notice" = "Opprettelsesdatoen kan være ukjent for noen chat."; + +"Settings.ShowRegDate" = "Vis registreringsdato"; +"Settings.ShowRegDate.Notice" = "Registreringsdatoen er ca."; + +"Settings.SendWithReturnKey" = "Send med 'retur'-tasten"; +"Settings.HidePhoneInSettingsUI" = "Skjul telefonen i innstillinger"; +"Settings.HidePhoneInSettingsUI.Notice" = "Dette vil bare skjule ditt telefonnummer for instillinger. For å skjule det for andre, gå til Personvern og Sikkerhet."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Hvis borte i 5 sekunder"; + +"ProxySettings.UseSystemDNS" = "Bruk system DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Bruk system DNS for å omgå timeout hvis du ikke har tilgang til Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Du **trenger ikke** %@!"; +"Common.RestartRequired" = "Omstart kreves"; +"Common.RestartNow" = "Omstart Nå"; +"Common.OpenTelegram" = "Åpne Telegram"; +"Common.UseTelegramForPremium" = "Vær oppmerksom på at for å få Telegram Premium, må du bruke den offisielle Telegram-appen. Når du har tatt Telegram Premium, vil alle funksjonene bli tilgjengelige i Swiftgram."; + +"Message.HoldToShowOrReport" = "Hold for å vise eller rapportere."; + +"Auth.AccountBackupReminder" = "Sørg for at du har en sikkerhetskopiert tilgangsmetode. Oppretthold en SIM for SMS eller en ekstra økt logget inn for å unngå å bli låst ute."; +"Auth.UnofficialAppCodeTitle" = "Du kan bare få koden med den offisielle appen"; + +"Settings.SmallReactions" = "Liten Reaksjon"; +"Settings.HideReactions" = "Skjul Reaksjoner"; + +"ContextMenu.SaveToCloud" = "Lagre til skyen"; +"ContextMenu.SelectFromUser" = "Velg fra forfatter"; + +"Settings.ContextMenu" = "KONTEKSTMENY"; +"Settings.ContextMenu.Notice" = "Deaktiverte oppføringer vil være tilgjengelige i 'Swiftgram'-undermenyen."; + + +"Settings.ChatSwipeOptions" = "Chat liste sveip alternativer"; +"Settings.DeleteChatSwipeOption" = "Sveip for å slette samtalen"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Dra til neste uleste kanal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Dra til neste emne"; +"Settings.GalleryCamera" = "Kamera i galleri"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" knapp"; +"Settings.SnapDeletionEffect" = "Sletting av melding effekter"; + +"Settings.Stickers.Size" = "STØRRELSE"; +"Settings.Stickers.Timestamp" = "Vis tidsstempel"; + +"Settings.RecordingButton" = "Tale opptaksknapp"; + +"Settings.DefaultEmojisFirst" = "Prioriter standard emojis"; +"Settings.DefaultEmojisFirst.Notice" = "Vis standard emojis før premium på emoji-tastaturet"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "opprettet: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Ble med %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrert"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dobbelttrykk for å redigere meldingen"; + +"Settings.wideChannelPosts" = "Brede innlegg i kanaler"; +"Settings.ForceEmojiTab" = "Emoji-tastatur som standard"; + +"Settings.forceBuiltInMic" = "Tving Mikrofon på enheten"; +"Settings.forceBuiltInMic.Notice" = "Hvis aktivert, vil appen bare bruke enhetens mikrofon selv om hodetelefoner er tilkoblet."; + +"Settings.hideChannelBottomButton" = "Skjul Kanal Bunnerpanel"; diff --git a/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..aec7e308ccc --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Ustawienia zawartości"; + +"Settings.Tabs.Header" = "ZAKŁADKI"; +"Settings.Tabs.HideTabBar" = "Ukryj pasek zakładek"; +"Settings.Tabs.ShowContacts" = "Pokaż zakładkę kontakty"; +"Settings.Tabs.ShowNames" = "Pokaż nazwy zakładek"; + +"Settings.Folders.BottomTab" = "Foldery na dole"; +"Settings.Folders.BottomTabStyle" = "Styl folderów na dole"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Ukryj \"%@\""; +"Settings.Folders.RememberLast" = "Otwórz ostatni folder"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram otworzy ostatnio używany folder po ponownym uruchomieniu lub zmianie konta"; + +"Settings.Folders.CompactNames" = "Mniejszy odstęp"; +"Settings.Folders.AllChatsTitle" = "Tytuł \"Wszystkie czaty\""; +"Settings.Folders.AllChatsTitle.short" = "Krótki"; +"Settings.Folders.AllChatsTitle.long" = "Długie"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Domyślny"; + + +"Settings.ChatList.Header" = "LISTA CZATU"; +"Settings.CompactChatList" = "Kompaktowa lista czatów"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Ukryj relacje"; +"Settings.Stories.WarnBeforeView" = "Pytaj przed wyświetleniem"; +"Settings.Stories.DisableSwipeToRecord" = "Wyłącz przeciągnij, aby nagrać"; + +"Settings.Translation.QuickTranslateButton" = "Przycisk Szybkie tłumaczenie"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Zobaczyć relację?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ BĘDZIE WIEDZIAŁ, że obejrzano jego relację."; +"Stories.Warning.NoticeStealth" = "%@ nie będzie wiedział, że obejrzano jego relację."; + +"Settings.Photo.Quality.Notice" = "Jakość wysyłanych zdjęć i fotorelacji"; +"Settings.Photo.SendLarge" = "Wyślij duże zdjęcia"; +"Settings.Photo.SendLarge.Notice" = "Zwiększ limit rozmiaru skompresowanych obrazów do 2560px"; + +"Settings.VideoNotes.Header" = "OKRĄGŁE WIDEO"; +"Settings.VideoNotes.StartWithRearCam" = "Uruchom z tylną kamerą"; + +"Settings.CustomColors.Header" = "KOLORY KONTA"; +"Settings.CustomColors.Saturation" = "NASYCENIE"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Ustaw nasycenie na 0%%, aby wyłączyć kolory konta"; + +"Settings.UploadsBoost" = "Przyśpieszenie wysyłania"; +"Settings.DownloadsBoost" = "Przyśpieszenie pobierania"; +"Settings.DownloadsBoost.none" = "Wyłączone"; +"Settings.DownloadsBoost.medium" = "Średnie"; +"Settings.DownloadsBoost.maximum" = "Maksymalne"; + +"Settings.ShowProfileID" = "Pokaż ID"; +"Settings.ShowDC" = "Pokaż centrum danych"; +"Settings.ShowCreationDate" = "Pokaż datę utworzenia czatu"; +"Settings.ShowCreationDate.Notice" = "Data utworzenia może być nieznana dla niektórych czatów."; + +"Settings.ShowRegDate" = "Pokaż datę rejestracji"; +"Settings.ShowRegDate.Notice" = "Data rejestracji jest przybliżona."; + +"Settings.SendWithReturnKey" = "Wyślij klawiszem „return”"; +"Settings.HidePhoneInSettingsUI" = "Ukryj numer telefonu w ustawieniach"; +"Settings.HidePhoneInSettingsUI.Notice" = "Twój numer zostanie ukryty tylko w interfejsie użytkownika. Aby ukryć go przed innymi, użyj ustawień prywatności."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Jeśli nieobecny przez 5 sekund"; + +"ProxySettings.UseSystemDNS" = "Użyj systemowego DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Użyj systemowego DNS, aby ominąć limit czasu, jeśli nie masz dostępu do Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Nie **potrzebujesz** %@!"; +"Common.RestartRequired" = "Wymagany restart"; +"Common.RestartNow" = "Uruchom teraz ponownie"; +"Common.OpenTelegram" = "Otwórz Telegram"; +"Common.UseTelegramForPremium" = "Pamiętaj, że aby otrzymać Telegram Premium, musisz skorzystać z oficjalnej aplikacji Telegram. Po uzyskaniu Telegram Premium wszystkie jego funkcje staną się dostępne w Swiftgram."; + +"Message.HoldToShowOrReport" = "Przytrzymaj, aby Pokazać lub Zgłosić."; + +"Auth.AccountBackupReminder" = "Upewnij się, że masz zapasową metodę dostępu. Zachowaj SIM do SMS-ów lub zalogowaną dodatkową sesję, aby uniknąć zablokowania."; +"Auth.UnofficialAppCodeTitle" = "Kod można uzyskać tylko za pomocą oficjalnej aplikacji"; + +"Settings.SmallReactions" = "Małe reakcje"; +"Settings.HideReactions" = "Ukryj Reakcje"; + +"ContextMenu.SaveToCloud" = "Zapisz w chmurze"; +"ContextMenu.SelectFromUser" = "Zaznacz od autora"; + +"Settings.ContextMenu" = "MENU KONTEKSTOWE"; +"Settings.ContextMenu.Notice" = "Wyłączone wpisy będą dostępne w podmenu „Swiftgram”."; + + +"Settings.ChatSwipeOptions" = "Opcje przesuwania listy czatów"; +"Settings.DeleteChatSwipeOption" = "Przesuń, aby usunąć czat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Pociągnij ➝ następny kanał"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Przeciągnij, aby przejść do następnego tematu"; +"Settings.GalleryCamera" = "Aparat w galerii"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Przycisk „%@”"; +"Settings.SnapDeletionEffect" = "Efekty usuwania wiadomości"; + +"Settings.Stickers.Size" = "WIELKOŚĆ"; +"Settings.Stickers.Timestamp" = "Pokaż znak czasu"; + +"Settings.RecordingButton" = "Przycisk głośności nagrywania"; + +"Settings.DefaultEmojisFirst" = "Wybierz standardowe emotikony"; +"Settings.DefaultEmojisFirst.Notice" = "Pokaż standardowe emotikony przed premium na klawiaturze emotikonów"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "utworzony: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Dołączył %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Zarejestrowane"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Podwójne stuknięcie, aby edytować wiadomość"; + +"Settings.wideChannelPosts" = "Szerokie posty w kanałach"; +"Settings.ForceEmojiTab" = "Klawiatura emoji domyślnie"; + +"Settings.forceBuiltInMic" = "Wymuś mikrofon urządzenia"; +"Settings.forceBuiltInMic.Notice" = "Jeśli ta opcja jest włączona, aplikacja będzie korzystać tylko z mikrofonu urządzenia nawet jeśli są podłączone słuchawki."; + +"Settings.hideChannelBottomButton" = "Ukryj dolny panel kanału"; diff --git a/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..1c99f3b00e8 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Configurações de Conteúdo"; + +"Settings.Tabs.Header" = "ABAS"; +"Settings.Tabs.HideTabBar" = "Ocultar Abas de Guias"; +"Settings.Tabs.ShowContacts" = "Mostrar Aba dos Contatos"; +"Settings.Tabs.ShowNames" = "Mostrar nomes das abas"; + +"Settings.Folders.BottomTab" = "Pastas embaixo"; +"Settings.Folders.BottomTabStyle" = "Estilos de Pastas Inferiores"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Ocultar \"%@\""; +"Settings.Folders.RememberLast" = "Abrir última pasta"; +"Settings.Folders.RememberLast.Notice" = "O Swiftgram abrirá a última pasta usada após reiniciar ou trocar de conta"; + +"Settings.Folders.CompactNames" = "Espaçamento Menor"; +"Settings.Folders.AllChatsTitle" = "Título \"Todos os bate-papos\""; +"Settings.Folders.AllChatsTitle.short" = "Curto"; +"Settings.Folders.AllChatsTitle.long" = "Longas"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Padrão"; + + +"Settings.ChatList.Header" = "LISTA DE CHAT"; +"Settings.CompactChatList" = "Lista de Bate-Papo Compacta"; + +"Settings.Profiles.Header" = "Perfis"; + +"Settings.Stories.Hide" = "Ocultar Stories"; +"Settings.Stories.WarnBeforeView" = "Perguntar antes de visualizar"; +"Settings.Stories.DisableSwipeToRecord" = "Desativar deslize para gravar"; + +"Settings.Translation.QuickTranslateButton" = "Botão de Tradução Rápida"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Ver Story?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SABERÁ que você viu a Story dele."; +"Stories.Warning.NoticeStealth" = "%@ não saberá que você viu a Story dele."; + +"Settings.Photo.Quality.Notice" = "Qualidade de fotos enviadas e photo-stories"; +"Settings.Photo.SendLarge" = "Enviar fotos grandes"; +"Settings.Photo.SendLarge.Notice" = "Aumentar o limite de tamanho de imagens comprimidas para 2560px"; + +"Settings.VideoNotes.Header" = "VÍDEOS REDONDOS"; +"Settings.VideoNotes.StartWithRearCam" = "Iniciar com a câmera traseira"; + +"Settings.CustomColors.Header" = "CORES DA CONTA"; +"Settings.CustomColors.Saturation" = "SATURAÇÃO"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Defina a saturação para 0%% para desativar as cores da conta"; + +"Settings.UploadsBoost" = "Aceleração de Uploads"; +"Settings.DownloadsBoost" = "Aceleração de Downloads"; +"Settings.DownloadsBoost.none" = "Desativado"; +"Settings.DownloadsBoost.medium" = "Médio"; +"Settings.DownloadsBoost.maximum" = "Máximo"; + +"Settings.ShowProfileID" = "Mostrar perfil"; +"Settings.ShowDC" = "Mostrar Centro de Dados"; +"Settings.ShowCreationDate" = "Mostrar data de criação do chat"; +"Settings.ShowCreationDate.Notice" = "A data de criação pode ser desconhecida para alguns chats."; + +"Settings.ShowRegDate" = "Mostrar data de registro"; +"Settings.ShowRegDate.Notice" = "A data de registo é aproximada."; + +"Settings.SendWithReturnKey" = "Enviar com a tecla \"retorno\""; +"Settings.HidePhoneInSettingsUI" = "Ocultar telefone nas configurações"; +"Settings.HidePhoneInSettingsUI.Notice" = "Seu número ficará oculto apenas na interface do usuário. Para ocultá-lo de outras pessoas, use as configurações de privacidade."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Se ausente por 5 segundos"; + +"ProxySettings.UseSystemDNS" = "Usar DNS do sistema"; +"ProxySettings.UseSystemDNS.Notice" = "Use o DNS do sistema para evitar tempo limite se você não tiver acesso ao DNS do Google"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Você **não precisa** de %@!"; +"Common.RestartRequired" = "Reinício necessário"; +"Common.RestartNow" = "Reiniciar agora"; +"Common.OpenTelegram" = "Abrir Telegram"; +"Common.UseTelegramForPremium" = "Observe que para obter o Telegram Premium, você precisa usar o aplicativo oficial do Telegram. Depois de obter o Telegram Premium, todos os seus recursos ficarão disponíveis no Swiftgram."; + +"Message.HoldToShowOrReport" = "Segure para Mostrar ou Denunciar."; + +"Auth.AccountBackupReminder" = "Certifique-se de ter um método de acesso de backup. Mantenha um SIM para SMS ou uma sessão adicional logada para evitar ser bloqueado."; +"Auth.UnofficialAppCodeTitle" = "Você só pode obter o código com o aplicativo oficial"; + +"Settings.SmallReactions" = "Pequenas reações"; +"Settings.HideReactions" = "Esconder Reações"; + +"ContextMenu.SaveToCloud" = "Salvar na Nuvem"; +"ContextMenu.SelectFromUser" = "Selecionar do Autor"; + +"Settings.ContextMenu" = "MENU DE CONTEXTO"; +"Settings.ContextMenu.Notice" = "Entradas desativadas estarão disponíveis no sub-menu 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "Opções de deslizar Lista de Chat"; +"Settings.DeleteChatSwipeOption" = "Deslize para excluir o bate-papo"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Puxe para o próximo canal não lido"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Arraste para o Próximo Tópico"; +"Settings.GalleryCamera" = "Câmera na Galeria"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Botão \"%@\""; +"Settings.SnapDeletionEffect" = "Efeitos de exclusão de mensagens"; + +"Settings.Stickers.Size" = "TAMANHO"; +"Settings.Stickers.Timestamp" = "Mostrar Data/Hora"; + +"Settings.RecordingButton" = "Botão de gravação de voz"; + +"Settings.DefaultEmojisFirst" = "Priorizar emojis padrão"; +"Settings.DefaultEmojisFirst.Notice" = "Mostrar emojis padrão antes dos premium no teclado de emojis"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "criado: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Entrou em %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrado"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Toque duplo para editar mensagem"; + +"Settings.wideChannelPosts" = "Postagens amplas nos canais"; +"Settings.ForceEmojiTab" = "Teclado de emojis por padrão"; + +"Settings.forceBuiltInMic" = "Forçar Microfone do Dispositivo"; +"Settings.forceBuiltInMic.Notice" = "Se ativado, o aplicativo usará apenas o microfone do dispositivo mesmo se os fones de ouvido estiverem conectados."; + +"Settings.hideChannelBottomButton" = "Ocultar Painel Inferior do Canal"; diff --git a/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..51084a4709e --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Setări Conținut"; + +"Settings.Tabs.Header" = "FERESTRE"; +"Settings.Tabs.HideTabBar" = "Ascunde bara de filă"; +"Settings.Tabs.ShowContacts" = "Vizualizare contacte"; +"Settings.Tabs.ShowNames" = "Arată Fereastra cu Numele"; + +"Settings.Folders.BottomTab" = "Dosare de jos"; +"Settings.Folders.BottomTabStyle" = "Stil directoare de jos"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegramă"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Ascundeți „%@\""; +"Settings.Folders.RememberLast" = "Deschideți ultimul dosar"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram va deschide ultimul folder utilizat atunci când reporniți aplicația sau schimbați conturile."; + +"Settings.Folders.CompactNames" = "Spațiere mai mică"; +"Settings.Folders.AllChatsTitle" = "Titlul \"Toate conversațiile\""; +"Settings.Folders.AllChatsTitle.short" = "Scurt"; +"Settings.Folders.AllChatsTitle.long" = "Lungă"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Implicit"; + + +"Settings.ChatList.Header" = "LISTA CHAT"; +"Settings.CompactChatList" = "Lista compactă de Chat"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Ascunde povestiri"; +"Settings.Stories.WarnBeforeView" = "Întreabă înainte de vizualizare"; +"Settings.Stories.DisableSwipeToRecord" = "Dezactivează glisarea pentru înregistrare"; + +"Settings.Translation.QuickTranslateButton" = "Butonul Traducere Rapidă"; + +"Stories.Warning.Author" = "Autor"; +"Stories.Warning.ViewStory" = "Vezi povestirea?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ VOR FI ACĂ SĂ VEDEȚI că le-ați văzut povestea lor."; +"Stories.Warning.NoticeStealth" = "%@ nu va putea vedea povestea lor."; + +"Settings.Photo.Quality.Notice" = "Calitatea fotografiilor și povestirilor încărcate."; +"Settings.Photo.SendLarge" = "Trimite fotografii mari"; +"Settings.Photo.SendLarge.Notice" = "Crește limita laterală a imaginilor comprimate la 2560px."; + +"Settings.VideoNotes.Header" = "VIDEO ROTUND"; +"Settings.VideoNotes.StartWithRearCam" = "Începe cu camera posterioară"; + +"Settings.CustomColors.Header" = "COLORTURI DE CONT"; +"Settings.CustomColors.Saturation" = "SATURARE"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Setați la 0%% pentru a dezactiva culorile contului."; + +"Settings.UploadsBoost" = "Accelerare Încărcare"; +"Settings.DownloadsBoost" = "Impuls descărcare"; +"Settings.DownloadsBoost.none" = "Dezactivat"; +"Settings.DownloadsBoost.medium" = "Medie"; +"Settings.DownloadsBoost.maximum" = "Maxim"; + +"Settings.ShowProfileID" = "Arată ID-ul profilului"; +"Settings.ShowDC" = "Arată Centrul de date"; +"Settings.ShowCreationDate" = "Arată data creării chat-ului"; +"Settings.ShowCreationDate.Notice" = "Data creării poate fi necunoscută pentru unele conversații."; + +"Settings.ShowRegDate" = "Arată data înregistrării"; +"Settings.ShowRegDate.Notice" = "Data înregistrării este aproximativă."; + +"Settings.SendWithReturnKey" = "Trimite cu cheia \"Returnare\""; +"Settings.HidePhoneInSettingsUI" = "Ascunde telefonul din setări"; +"Settings.HidePhoneInSettingsUI.Notice" = "Acest lucru va ascunde numărul de telefon din interfața de setări. Pentru a-l ascunde de alții, mergi la confidențialitate și securitate."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Dacă este plecat timp de 5 secunde"; + +"ProxySettings.UseSystemDNS" = "Utilizați DNS sistem"; +"ProxySettings.UseSystemDNS.Notice" = "Utilizați DNS pentru a ocoli timeout-ul dacă nu aveți acces la Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Nu ai nevoie de ** %@!"; +"Common.RestartRequired" = "Repornire necesară"; +"Common.RestartNow" = "Repornește acum"; +"Common.OpenTelegram" = "Deschide telegrama"; +"Common.UseTelegramForPremium" = "Vă rugăm să reţineţi că, pentru a obţine Telegram Premium, trebuie să utilizaţi aplicaţia oficială Telegram. Odată ce ai obţinut Telegram Premium, toate caracteristicile sale vor deveni disponibile în Swiftgram."; + +"Message.HoldToShowOrReport" = "Țineți apăsat pentru a afișa sau raporta."; + +"Auth.AccountBackupReminder" = "Asigurați-vă că aveți o metodă de acces de rezervă. Păstrați un SIM pentru SMS sau o sesiune adițională conectată pentru a evita blocarea."; +"Auth.UnofficialAppCodeTitle" = "Poți obține codul doar cu aplicația oficială"; + +"Settings.SmallReactions" = "Reacţii mici"; +"Settings.HideReactions" = "Ascunde Reacțiile"; + +"ContextMenu.SaveToCloud" = "Salvează în Cloud"; +"ContextMenu.SelectFromUser" = "Selectați din autor"; + +"Settings.ContextMenu" = "MENIU CONTEXTUAL"; +"Settings.ContextMenu.Notice" = "Intrările dezactivate vor fi disponibile în submeniul 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "Opțiuni de glisare a chatului"; +"Settings.DeleteChatSwipeOption" = "Glisați pentru ștergere chat"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Trageţi pentru următorul canal necitit"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Trageți către Următorul Subiect"; +"Settings.GalleryCamera" = "Cameră foto în Galerie"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Butonul \"%@\""; +"Settings.SnapDeletionEffect" = "Efecte ștergere mesaj"; + +"Settings.Stickers.Size" = "MISIUNE"; +"Settings.Stickers.Timestamp" = "Arată Ora"; + +"Settings.RecordingButton" = "Butonul Înregistrare Voce"; + +"Settings.DefaultEmojisFirst" = "Prioritize emoticoanele standard"; +"Settings.DefaultEmojisFirst.Notice" = "Afișați emoticoanele standard înainte de cele premium în tastatura emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "creat: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "S-a alăturat %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Înregistrat"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Apăsați de două ori pentru a edita mesajul"; + +"Settings.wideChannelPosts" = "Postări late în canale"; +"Settings.ForceEmojiTab" = "Tastatură emoji implicită"; + +"Settings.forceBuiltInMic" = "Forțează Microfon Dispozitiv"; +"Settings.forceBuiltInMic.Notice" = "Dacă este activat, aplicația va folosi doar microfonul dispozitivului chiar dacă sunt conectate căștile."; + +"Settings.hideChannelBottomButton" = "Ascundeți panoul de jos al canalului"; diff --git a/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..42d47057010 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Настройки контента"; + +"Settings.Tabs.Header" = "ВКЛАДКИ"; +"Settings.Tabs.HideTabBar" = "Скрыть панель вкладок"; +"Settings.Tabs.ShowContacts" = "Вкладка «Контакты»"; +"Settings.Tabs.ShowNames" = "Имена вкладок"; + +"Settings.Folders.BottomTab" = "Папки снизу"; +"Settings.Folders.BottomTabStyle" = "Стиль папок внизу"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Скрыть \"%@\""; +"Settings.Folders.RememberLast" = "Открывать последнюю папку"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram откроет последнюю использованную папку после перезапуска или переключения учетной записи"; + +"Settings.Folders.CompactNames" = "Уменьшенные расстояния"; +"Settings.Folders.AllChatsTitle" = "Название \"Все чаты\""; +"Settings.Folders.AllChatsTitle.short" = "Короткое"; +"Settings.Folders.AllChatsTitle.long" = "Длинное"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "По умолчанию"; + + +"Settings.ChatList.Header" = "СПИСОК ЧАТОВ"; +"Settings.CompactChatList" = "Компактный список чатов"; + +"Settings.Profiles.Header" = "ПРОФИЛИ"; + +"Settings.Stories.Hide" = "Скрыть истории"; +"Settings.Stories.WarnBeforeView" = "Спросить перед просмотром"; +"Settings.Stories.DisableSwipeToRecord" = "Отключить свайп для записи"; + +"Settings.Translation.QuickTranslateButton" = "Кнопка быстрого перевода"; + +"Stories.Warning.Author" = "Автор"; +"Stories.Warning.ViewStory" = "Просмотреть историю?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ СМОЖЕТ УВИДЕТЬ, что вы просмотрели историю."; +"Stories.Warning.NoticeStealth" = "%@ не сможет увидеть, что вы просмотрели историю."; + +"Settings.Photo.Quality.Notice" = "Качество исходящих фото и фото-историй"; +"Settings.Photo.SendLarge" = "Отправлять большие фото"; +"Settings.Photo.SendLarge.Notice" = "Увеличить лимит сторон для сжатых фото до 2560пкс"; + +"Settings.VideoNotes.Header" = "КРУГЛЫЕ ВИДЕО"; +"Settings.VideoNotes.StartWithRearCam" = "На заднюю камеру"; + +"Settings.CustomColors.Header" = "ПЕРСОНАЛЬНЫЕ ЦВЕТА"; +"Settings.CustomColors.Saturation" = "НАСЫЩЕННОСТЬ"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Установите насыщенность на 0%%, чтобы отключить персональные цвета"; + +"Settings.UploadsBoost" = "Ускорение загрузки"; +"Settings.DownloadsBoost" = "Ускорение скачивания"; +"Settings.DownloadsBoost.none" = "Выключено"; +"Settings.DownloadsBoost.medium" = "Средне"; +"Settings.DownloadsBoost.maximum" = "Максимум"; + +"Settings.ShowProfileID" = "ID профилей"; +"Settings.ShowDC" = "Показать дата-центр (DC)"; +"Settings.ShowCreationDate" = "Показать дату создания чата"; +"Settings.ShowCreationDate.Notice" = "Дата создания может быть неизвестна для некоторых чатов."; + +"Settings.ShowRegDate" = "Показать дату регистрации"; +"Settings.ShowRegDate.Notice" = "Дата регистрации приблизительная."; + +"Settings.SendWithReturnKey" = "Отправка кнопкой \"Ввод\""; +"Settings.HidePhoneInSettingsUI" = "Скрыть номер"; +"Settings.HidePhoneInSettingsUI.Notice" = "Ваш номер будет скрыт только в интерфейсе настроек. Используйте настройки Конфиденциальности, чтобы скрыть его от других."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Через 5 секунд"; + +"ProxySettings.UseSystemDNS" = "Системный DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Используйте системный DNS, чтобы избежать задержки, если у вас нет доступа к DNS Google"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Вам **не нужен** %@!"; +"Common.RestartRequired" = "Необходим перезапуск"; +"Common.RestartNow" = "Перезапустить Сейчас"; +"Common.OpenTelegram" = "Открыть Telegram"; +"Common.UseTelegramForPremium" = "Обратите внимание, что для получения Telegram Premium, вы должны использовать официальное приложение Telegram. Как только вы получите Telegram Premium, все его функции станут доступны в Swiftgram."; + +"Message.HoldToShowOrReport" = "Удерживайте для Показа или Жалобы."; + +"Auth.AccountBackupReminder" = "Убедитесь, что у вас есть запасной вариант входа: Активная SIM-карта или дополнительная сессия, чтобы не потерять доступ к аккаунту."; +"Auth.UnofficialAppCodeTitle" = "Вы можете получить код только в официальном приложении"; + +"Settings.SmallReactions" = "Маленькие реакции"; +"Settings.HideReactions" = "Скрыть реакции"; + +"ContextMenu.SaveToCloud" = "Сохранить в Избранное"; +"ContextMenu.SelectFromUser" = "Выбрать от Автора"; + +"Settings.ContextMenu" = "КОНТЕКСТНОЕ МЕНЮ"; +"Settings.ContextMenu.Notice" = "Выключенные пункты будут доступны в подменю «Swiftgram»."; + + +"Settings.ChatSwipeOptions" = "Опции чатов при свайпе"; +"Settings.DeleteChatSwipeOption" = "Свайп для удаления чата"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Потянуть для перехода в канал"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Потянуть для перехода к след. теме"; +"Settings.GalleryCamera" = "Камера в галерее"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Кнопка \"%@\""; +"Settings.SnapDeletionEffect" = "Эффекты удаления сообщений"; + +"Settings.Stickers.Size" = "РАЗМЕР"; +"Settings.Stickers.Timestamp" = "Показывать время"; + +"Settings.RecordingButton" = "Кнопка записи голоса"; + +"Settings.DefaultEmojisFirst" = "Приоритизировать стандартные эмодзи"; +"Settings.DefaultEmojisFirst.Notice" = "Показывать стандартные эмодзи перед Premium в эмодзи-клавиатуре"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "создан: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Присоединился к %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Дата регистрации"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Редактирование двойным тапом"; + +"Settings.wideChannelPosts" = "Широкие посты в каналах"; +"Settings.ForceEmojiTab" = "Клавиатура с эмодзи по умолчанию"; + +"Settings.forceBuiltInMic" = "Всегда использовать микрофон устройства"; +"Settings.forceBuiltInMic.Notice" = "Если включено, то приложение будет использовать только встроенный микрофон устройства, даже если подключены наушники."; + +"Settings.hideChannelBottomButton" = "Скрыть нижнюю панель в каналах"; diff --git a/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..869c70ba7ea --- /dev/null +++ b/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings @@ -0,0 +1,2 @@ +"Settings.Tabs.Header" = "පටිති"; +"ContextMenu.SaveToCloud" = "මේඝයට සුරකින්න"; diff --git a/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..77376339e39 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings @@ -0,0 +1,4 @@ +"Settings.Tabs.Header" = "ZÁLOŽKY"; +"Settings.Tabs.ShowContacts" = "Zobraziť kontakty"; +"Settings.Tabs.ShowNames" = "Zobraziť názvy záložiek"; +"ContextMenu.SaveToCloud" = "Uložiť na Cloud"; diff --git a/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..f48381ea1b6 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Подешавања садржаја"; + +"Settings.Tabs.Header" = "ТАБОВИ"; +"Settings.Tabs.HideTabBar" = "Сакриј Таб бар"; +"Settings.Tabs.ShowContacts" = "Прикажи таб Контакти"; +"Settings.Tabs.ShowNames" = "Прикажи имена табова"; + +"Settings.Folders.BottomTab" = "Фасцикле у дну"; +"Settings.Folders.BottomTabStyle" = "Стил фасцикли у дну"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Телеграм"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Сакриј \"%@\""; +"Settings.Folders.RememberLast" = "Отвори последњу фасциклу"; +"Settings.Folders.RememberLast.Notice" = "Свифтграм ће отворити последње коришћену фасциклу када поново покренете апликацију или измените налоге."; + +"Settings.Folders.CompactNames" = "Мањи размак"; +"Settings.Folders.AllChatsTitle" = "Наслов \"Сви Четови\""; +"Settings.Folders.AllChatsTitle.short" = "Кратко"; +"Settings.Folders.AllChatsTitle.long" = "Дуго"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Подразумевано"; + + +"Settings.ChatList.Header" = "ЛИСТА ЧЕТОВА"; +"Settings.CompactChatList" = "Компактна листа чета"; + +"Settings.Profiles.Header" = "ПРОФИЛИ"; + +"Settings.Stories.Hide" = "Сакриј приче"; +"Settings.Stories.WarnBeforeView" = "Питај пре прегледања"; +"Settings.Stories.DisableSwipeToRecord" = "Онемогући превлачење за снимање"; + +"Settings.Translation.QuickTranslateButton" = "Дугме за брзо превођење"; + +"Stories.Warning.Author" = "Аутор"; +"Stories.Warning.ViewStory" = "Погледај причу?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ ЋЕ ВИДЕТИ да сте видели њихову причу."; +"Stories.Warning.NoticeStealth" = "%@ неће моћи видети да сте видели њихову причу."; + +"Settings.Photo.Quality.Notice" = "Квалитет постављених фотографија и приказа."; +"Settings.Photo.SendLarge" = "Пошаљи велике фотографије"; +"Settings.Photo.SendLarge.Notice" = "Повећај лимит величине за компресоване слике на 2560пкс."; + +"Settings.VideoNotes.Header" = "КРУГ ВИДЕО"; +"Settings.VideoNotes.StartWithRearCam" = "Почни са задњом камером"; + +"Settings.CustomColors.Header" = "БОЈЕ НАЛОГА"; +"Settings.CustomColors.Saturation" = "ЗАСИЋЕЊЕ"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Поставите на 0%% да онемогућите боје налога."; + +"Settings.UploadsBoost" = "Појачај поставке поставки"; +"Settings.DownloadsBoost" = "Преузми појачање"; +"Settings.DownloadsBoost.none" = "Онемогућено"; +"Settings.DownloadsBoost.medium" = "Средње"; +"Settings.DownloadsBoost.maximum" = "Максимално"; + +"Settings.ShowProfileID" = "Прикажи идентификациони број профила"; +"Settings.ShowDC" = "Прикажи центар података"; +"Settings.ShowCreationDate" = "Прикажи датум креирања чата"; +"Settings.ShowCreationDate.Notice" = "Можда није познат датум креирања за неке разговоре."; + +"Settings.ShowRegDate" = "Прикажи датум регистрације"; +"Settings.ShowRegDate.Notice" = "Датум регистрације је приближан."; + +"Settings.SendWithReturnKey" = "Пошаљи са 'повратак' тастером"; +"Settings.HidePhoneInSettingsUI" = "Сакриј телефон у поставкама"; +"Settings.HidePhoneInSettingsUI.Notice" = "Ово само ће скрити ваш број телефона из интерфејса поставки. Да бисте га скрили од других, идите на Приватност и безбедност."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Ако је одсутан 5 секунди"; + +"ProxySettings.UseSystemDNS" = "Користи системски DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Користи системски DNS да заобиђеш временски лимит ако немаш приступ Google DNS-у"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Не треба вам **%@**!"; +"Common.RestartRequired" = "Потребно поновно покретање"; +"Common.RestartNow" = "Поново покрени сада"; +"Common.OpenTelegram" = "Отвори Телеграм"; +"Common.UseTelegramForPremium" = "Обратите пажњу да бисте добили Телеграм Премијум, морате користити официјалну Телеграм апликацију. Након што стечете Телеграм Премијум, све његове функције ће бити доступне у Свифтграму."; + +"Message.HoldToShowOrReport" = "Држи да би показао или пријавио."; + +"Auth.AccountBackupReminder" = "Обезбеди да имаш методу приступа за резерву. Задржи СИМ за СМС или додатну сесију пријављену да избегнеш блокирање."; +"Auth.UnofficialAppCodeTitle" = "Код можете добити само са званичном апликацијом"; + +"Settings.SmallReactions" = "Мале реакције"; +"Settings.HideReactions" = "Сакриј реакције"; + +"ContextMenu.SaveToCloud" = "Сачувај у облак"; +"ContextMenu.SelectFromUser" = "Изабери од аутора"; + +"Settings.ContextMenu" = "КОНТЕКСТ МЕНИ"; +"Settings.ContextMenu.Notice" = "Онемогућени уноси ће бити доступни у 'Swiftgram' подменују."; + + +"Settings.ChatSwipeOptions" = "Опције превлачења списка разговора"; +"Settings.DeleteChatSwipeOption" = "Превучите за брисање чет"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Повуци на следећи непрочитан канал"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Повуци на следећу тему"; +"Settings.GalleryCamera" = "Камера у галерији"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Дугме"; +"Settings.SnapDeletionEffect" = "Ефекти брисања поруке"; + +"Settings.Stickers.Size" = "ВЕЛИЧИНА"; +"Settings.Stickers.Timestamp" = "Прикажи временски линку"; + +"Settings.RecordingButton" = "Дугме за гласовно снимање"; + +"Settings.DefaultEmojisFirst" = "Приоритизовати стандардне емотиконе"; +"Settings.DefaultEmojisFirst.Notice" = "Прикажи стандардне емотиконе пре премијумских на тастатури емотикона"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "креирано: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Придружен: %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Регистрован"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Двоструки додир за уређивање поруке"; + +"Settings.wideChannelPosts" = "Широки постови у каналима"; +"Settings.ForceEmojiTab" = "Емоџи тастатура по подразумеваној подешавања"; + +"Settings.forceBuiltInMic" = "Наметни микрофон уређаја"; +"Settings.forceBuiltInMic.Notice" = "Ако је омогућено, апликација ће користити само микрофон уређаја чак и ако су прикључене слушалице."; + +"Settings.hideChannelBottomButton" = "Сакриј донји панел канала"; diff --git a/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..d16c84319bd --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Innehållsinställningar"; + +"Settings.Tabs.Header" = "Flikar"; +"Settings.Tabs.HideTabBar" = "Dölj flikfält"; +"Settings.Tabs.ShowContacts" = "Visa Kontakter-flik"; +"Settings.Tabs.ShowNames" = "Show Tab Names"; + +"Settings.Folders.BottomTab" = "Mappar längst ner"; +"Settings.Folders.BottomTabStyle" = "Stil på nedre mappar"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Dölj \"%@\""; +"Settings.Folders.RememberLast" = "Öppna senaste mapp"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram öppnar den senast använda mappen när du startar om appen eller byter konton."; + +"Settings.Folders.CompactNames" = "Mindre avstånd"; +"Settings.Folders.AllChatsTitle" = "\"Alla chattar\" titel"; +"Settings.Folders.AllChatsTitle.short" = "Kort"; +"Settings.Folders.AllChatsTitle.long" = "Lång"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standard"; + + +"Settings.ChatList.Header" = "CHATT LISTA"; +"Settings.CompactChatList" = "Kompakt chattlista"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Dölj Berättelser"; +"Settings.Stories.WarnBeforeView" = "Fråga innan du tittar"; +"Settings.Stories.DisableSwipeToRecord" = "Inaktivera svep för att spela in"; + +"Settings.Translation.QuickTranslateButton" = "Snabböversättningsknapp"; + +"Stories.Warning.Author" = "Författare"; +"Stories.Warning.ViewStory" = "Visa Berättelse?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ KOMMER ATT SE att du har sett deras Berättelse."; +"Stories.Warning.NoticeStealth" = "%@ kommer inte att se att du har sett deras Berättelse."; + +"Settings.Photo.Quality.Notice" = "Kvaliteten på uppladdade bilder och berättelser."; +"Settings.Photo.SendLarge" = "Skicka stora foton"; +"Settings.Photo.SendLarge.Notice" = "Öka sidogränsen för komprimerade bilder till 2560px."; + +"Settings.VideoNotes.Header" = "RUND VIDEO"; +"Settings.VideoNotes.StartWithRearCam" = "Börja med bakre kamera"; + +"Settings.CustomColors.Header" = "KONTOFÄRGER"; +"Settings.CustomColors.Saturation" = "MÄTTNING"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Sätt till 0%% för att inaktivera kontofärger."; + +"Settings.UploadsBoost" = "Uppladdningshastighet"; +"Settings.DownloadsBoost" = "Ladda ner Boost"; +"Settings.DownloadsBoost.none" = "Inaktiverad"; +"Settings.DownloadsBoost.medium" = "Medium"; +"Settings.DownloadsBoost.maximum" = "Maximal"; + +"Settings.ShowProfileID" = "Visa profil-ID"; +"Settings.ShowDC" = "Visa datacenter"; +"Settings.ShowCreationDate" = "Visa datum för att skapa chatt"; +"Settings.ShowCreationDate.Notice" = "Skapandedatumet kan vara okänt för vissa chattar."; + +"Settings.ShowRegDate" = "Visa registreringsdatum"; +"Settings.ShowRegDate.Notice" = "Registreringsdatumet är ungefärligt."; + +"Settings.SendWithReturnKey" = "Skicka med 'retur'-tangenten"; +"Settings.HidePhoneInSettingsUI" = "Dölj telefon i inställningar"; +"Settings.HidePhoneInSettingsUI.Notice" = "Detta döljer endast ditt telefonnummer från inställningsgränssnittet. För att dölja det från andra, gå till Sekretess och säkerhet."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Om borta i 5 sekunder"; + +"ProxySettings.UseSystemDNS" = "Använd system-DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Använd system-DNS för att kringgå timeout om du inte har tillgång till Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Du **behöver inte** %@!"; +"Common.RestartRequired" = "Omstart krävs"; +"Common.RestartNow" = "Starta om Nu"; +"Common.OpenTelegram" = "Öppna Telegram"; +"Common.UseTelegramForPremium" = "Observera att för att få Telegram Premium måste du använda den officiella Telegram-appen. När du har fått Telegram Premium, kommer alla dess funktioner att bli tillgängliga i Swiftgram."; + +"Message.HoldToShowOrReport" = "Håll in för att Visa eller Rapportera."; + +"Auth.AccountBackupReminder" = "Se till att du har en backup-åtkomstmetod. Behåll ett SIM för SMS eller en extra session inloggad för att undvika att bli utelåst."; +"Auth.UnofficialAppCodeTitle" = "Du kan endast få koden med den officiella appen"; + +"Settings.SmallReactions" = "Små reaktioner"; +"Settings.HideReactions" = "Dölj Reaktioner"; + +"ContextMenu.SaveToCloud" = "Spara till Molnet"; +"ContextMenu.SelectFromUser" = "Välj från Författaren"; + +"Settings.ContextMenu" = "KONTEXTMENY"; +"Settings.ContextMenu.Notice" = "Inaktiverade poster kommer att vara tillgängliga i 'Swiftgram'-undermenyn."; + + +"Settings.ChatSwipeOptions" = "Svepalternativ för chattlistan"; +"Settings.DeleteChatSwipeOption" = "Svep för att ta bort chatt"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Dra till nästa olästa kanal"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Dra till Nästa Ämne"; +"Settings.GalleryCamera" = "Kamera i galleriet"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Knapp"; +"Settings.SnapDeletionEffect" = "Effekter på meddelandet"; + +"Settings.Stickers.Size" = "SIZE"; +"Settings.Stickers.Timestamp" = "Visa tidsstämpel"; + +"Settings.RecordingButton" = "Röstinspelningsknapp"; + +"Settings.DefaultEmojisFirst" = "Prioritera standardemojis"; +"Settings.DefaultEmojisFirst.Notice" = "Visa standardemojis innan premium i emoji-tangentbordet"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "skapad: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Gick med %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Registrerad"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Dubbeltryck för att redigera meddelandet"; + +"Settings.wideChannelPosts" = "Bredda inlägg i kanaler"; +"Settings.ForceEmojiTab" = "Emoji-tangentbord som standard"; + +"Settings.forceBuiltInMic" = "Tvinga enhetsmikrofonen"; +"Settings.forceBuiltInMic.Notice" = "Om aktiverat, kommer appen endast använda enhetens mikrofon även om hörlurar är anslutna."; + +"Settings.hideChannelBottomButton" = "Dölj kanalle bottenpanel"; diff --git a/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..40941fae715 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "İçerik Ayarları"; + +"Settings.Tabs.Header" = "SEKMELER"; +"Settings.Tabs.HideTabBar" = "Sekme çubuğunu gizle"; +"Settings.Tabs.ShowContacts" = "Kişiler Sekmesini Göster"; +"Settings.Tabs.ShowNames" = "Sekme isimlerini göster"; + +"Settings.Folders.BottomTab" = "Altta klasörler"; +"Settings.Folders.BottomTabStyle" = "Alt klasör stili"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "\"%@\" Gizle"; +"Settings.Folders.RememberLast" = "Son klasörü aç"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram, yeniden başlatıldıktan ya da hesap değişiminden sonra son kullanılan klasörü açacaktır"; + +"Settings.Folders.CompactNames" = "Daha küçük aralık"; +"Settings.Folders.AllChatsTitle" = "\"Tüm Sohbetler\" başlığı"; +"Settings.Folders.AllChatsTitle.short" = "Kısa"; +"Settings.Folders.AllChatsTitle.long" = "Uzun"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Varsayılan"; + + +"Settings.ChatList.Header" = "SOHBET LİSTESİ"; +"Settings.CompactChatList" = "Kompakt Sohbet Listesi"; + +"Settings.Profiles.Header" = "PROFİLLER"; + +"Settings.Stories.Hide" = "Hikayeleri Gizle"; +"Settings.Stories.WarnBeforeView" = "Görüntülemeden önce sor"; +"Settings.Stories.DisableSwipeToRecord" = "Kaydetmek için kaydırmayı devre dışı bırak"; + +"Settings.Translation.QuickTranslateButton" = "Hızlı Çeviri butonu"; + +"Stories.Warning.Author" = "Yazar"; +"Stories.Warning.ViewStory" = "Hikayeyi Görüntüle?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@, Hikayesini görüntülediğinizi GÖREBİLECEK."; +"Stories.Warning.NoticeStealth" = "%@, hikayesini görüntülediğinizi göremeyecek."; + +"Settings.Photo.Quality.Notice" = "Gönderilen fotoğrafların ve foto-hikayelerin kalitesi"; +"Settings.Photo.SendLarge" = "Büyük fotoğraflar gönder"; +"Settings.Photo.SendLarge.Notice" = "Sıkıştırılmış resimlerdeki kenar sınırını 2560 piksele çıkar"; + +"Settings.VideoNotes.Header" = "YUVARLAK VİDEOLAR"; +"Settings.VideoNotes.StartWithRearCam" = "Arka kamerayla başlat"; + +"Settings.CustomColors.Header" = "HESAP RENKLERİ"; +"Settings.CustomColors.Saturation" = "DOYUM"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Hesap renklerini devre dışı bırakmak için doyumu 0%%'a ayarlayın"; + +"Settings.UploadsBoost" = "Karşıya yüklemeleri hızlandır"; +"Settings.DownloadsBoost" = "İndirmeleri hızlandır"; +"Settings.DownloadsBoost.none" = "Devre dışı"; +"Settings.DownloadsBoost.medium" = "Orta"; +"Settings.DownloadsBoost.maximum" = "En fazla"; + +"Settings.ShowProfileID" = "Profil ID'sini Göster"; +"Settings.ShowDC" = "Veri Merkezini Göster"; +"Settings.ShowCreationDate" = "Sohbet Oluşturma Tarihini Göster"; +"Settings.ShowCreationDate.Notice" = "Bazı sohbetler için oluşturma tarihi bilinmeyebilir."; + +"Settings.ShowRegDate" = "Kaydolma Tarihini Göster"; +"Settings.ShowRegDate.Notice" = "Kaydolma tarihi yaklaşık olarak belirtilmiştir."; + +"Settings.SendWithReturnKey" = "\"enter\" tuşu ile gönder"; +"Settings.HidePhoneInSettingsUI" = "Ayarlarda numarayı gizle"; +"Settings.HidePhoneInSettingsUI.Notice" = "Numaranız sadece arayüzde gizlenecek. Diğerlerinden gizlemek için, lütfen Gizlilik ayarlarını kullanın."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "5 saniye uzakta kalırsanız"; + +"ProxySettings.UseSystemDNS" = "Sistem DNS'sini kullan"; +"ProxySettings.UseSystemDNS.Notice" = "Google DNS'ye erişiminiz yoksa, zaman aşımını aşmak için sistem DNS'sini kullanın"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "%@ **gerekmez**!"; +"Common.RestartRequired" = "Yeniden başlatma gerekli"; +"Common.RestartNow" = "Şimdi Yeniden Başlat"; +"Common.OpenTelegram" = "Telegram'ı Aç"; +"Common.UseTelegramForPremium" = "Unutmayın ki Telegram Premium'u edinmek için resmî Telegram uygulamasını kullanmanız gerekmektedir. Telegram Premium sahibi olduktan sonra onun tüm özellikleri Swiftgram'da mevcut olacaktır."; + +"Message.HoldToShowOrReport" = "Göstermek veya Bildirmek için Basılı Tutun."; + +"Auth.AccountBackupReminder" = "Yedek erişim yönteminiz olduğundan emin olun. Kilitlenmeden kaçınmak için bir SIM kartı saklayın veya ek bir oturum açın."; +"Auth.UnofficialAppCodeTitle" = "Kodu yalnızca resmi uygulamadan edinebilirsiniz"; + +"Settings.SmallReactions" = "Küçük tepkiler"; +"Settings.HideReactions" = "Tepkileri Gizle"; + +"ContextMenu.SaveToCloud" = "Buluta Kaydet"; +"ContextMenu.SelectFromUser" = "Yazardan Seç"; + +"Settings.ContextMenu" = "BAĞLAM MENÜSÜ"; +"Settings.ContextMenu.Notice" = "Devre dışı bırakılmış girişler \"Swiftgram\" alt menüsünde mevcut olacaktır."; + + +"Settings.ChatSwipeOptions" = "Sohbet listesi kaydırma seçenekleri"; +"Settings.DeleteChatSwipeOption" = "Sohbete Silmek İçin Kaydırın"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Sonraki okunmamış kanal için çekin"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Bir Sonraki Konuya Çek"; +"Settings.GalleryCamera" = "Galeride kamera"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" butonu"; +"Settings.SnapDeletionEffect" = "Mesaj silme efektleri"; + +"Settings.Stickers.Size" = "BOYUT"; +"Settings.Stickers.Timestamp" = "Zaman Damgasını Göster"; + +"Settings.RecordingButton" = "Ses Kaydı Düğmesi"; + +"Settings.DefaultEmojisFirst" = "Standart emojileri önceliklendirin"; +"Settings.DefaultEmojisFirst.Notice" = "Emoji klavyesinde premiumdan önce standart emojileri göster"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "oluşturuldu: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Katıldı: %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Kayıtlı"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Mesajı düzenlemek için çift dokunun"; + +"Settings.wideChannelPosts" = "Kanallardaki geniş gönderiler"; +"Settings.ForceEmojiTab" = "Varsayılan olarak Emoji klavyesi"; + +"Settings.forceBuiltInMic" = "Cihaz Mikrofonunu Zorla"; +"Settings.forceBuiltInMic.Notice" = "Etkinleştirildiğinde, uygulama kulaklıklar bağlı olsa bile sadece cihaz mikrofonunu kullanacaktır."; + +"Settings.hideChannelBottomButton" = "Kanal Alt Panelini Gizle"; diff --git a/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..242314532d7 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings @@ -0,0 +1,148 @@ +"Settings.ContentSettings" = "Налаштування контенту"; + +"Settings.Tabs.Header" = "ВКЛАДКИ"; +"Settings.Tabs.HideTabBar" = "Приховати панель вкладок"; +"Settings.Tabs.ShowContacts" = "Вкладка \"Контакти\""; +"Settings.Tabs.ShowNames" = "Показувати назви вкладок"; + +"Settings.Folders.BottomTab" = "Папки знизу"; +"Settings.Folders.BottomTabStyle" = "Стиль нижніх папок"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Приховати \"%@\""; +"Settings.Folders.RememberLast" = "Відкривати останню папку"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram відкриє останню папку після перезапуску застосунку або зміни акаунту."; + +"Settings.Folders.CompactNames" = "Зменшити відступи"; +"Settings.Folders.AllChatsTitle" = "Заголовок \"Усі чати\""; +"Settings.Folders.AllChatsTitle.short" = "Короткий"; +"Settings.Folders.AllChatsTitle.long" = "Довгий"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Типовий"; + + +"Settings.ChatList.Header" = "СПИСОК ЧАТІВ"; +"Settings.CompactChatList" = "Компактний список чатів"; + +"Settings.Profiles.Header" = "ПРОФІЛІ"; + +"Settings.Stories.Hide" = "Приховувати історії"; +"Settings.Stories.WarnBeforeView" = "Питати перед переглядом"; +"Settings.Stories.DisableSwipeToRecord" = "Вимкнути \"Свайп для запису\""; + +"Settings.Translation.QuickTranslateButton" = "Кнопка швидкого перекладу"; + +"Stories.Warning.Author" = "Автор"; +"Stories.Warning.ViewStory" = "Переглянути історію?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ ЗМОЖЕ ПОБАЧИТИ, що ви переглянули їх історію."; +"Stories.Warning.NoticeStealth" = "%@ не побачить, що ви переглянули їх історію."; + +"Settings.Photo.Quality.Notice" = "Якість відправлених фото та історій"; +"Settings.Photo.SendLarge" = "Надсилати великі фотографії"; +"Settings.Photo.SendLarge.Notice" = "Збільшити ліміт розміру стиснутих зображень до 2560px"; + +"Settings.VideoNotes.Header" = "КРУГЛІ ВІДЕО"; +"Settings.VideoNotes.StartWithRearCam" = "Починати запис з задньої камери"; + +"Settings.CustomColors.Header" = "КОЛЬОРИ АККАУНТУ"; +"Settings.CustomColors.Saturation" = "НАСИЧЕНІСТЬ"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Встановіть насиченість на 0%%, щоб вимкнути персональні кольори"; + +"Settings.UploadsBoost" = "Прискорення вивантаження"; +"Settings.DownloadsBoost" = "Прискорення завантаження"; +"Settings.DownloadsBoost.none" = "Відключено"; +"Settings.DownloadsBoost.medium" = "Середнє"; +"Settings.DownloadsBoost.maximum" = "Максимальне"; + +"Settings.ShowProfileID" = "Показувати ID профілю"; +"Settings.ShowDC" = "Показувати датацентр"; +"Settings.ShowCreationDate" = "Показувати дату створення чату"; +"Settings.ShowCreationDate.Notice" = "Дата створення може бути невідома для деяких чатів."; + +"Settings.ShowRegDate" = "Показувати дату реєстрації"; +"Settings.ShowRegDate.Notice" = "Дата реєстрації є приблизною."; + +"Settings.SendWithReturnKey" = "Надсилати кнопкою \"Введення\""; +"Settings.HidePhoneInSettingsUI" = "Приховати телефон у налаштуваннях"; +"Settings.HidePhoneInSettingsUI.Notice" = "Номер буде прихований тільки в налаштуваннях. Перейдіть в \"Приватність і безпека\", щоб приховати його від інших."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "За 5 сек"; + +"ProxySettings.UseSystemDNS" = "Використовувати системні налаштування DNS"; +"ProxySettings.UseSystemDNS.Notice" = "Використовувати системний DNS для обходу тайм-ауту, якщо у вас немає доступу до Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Вам **не потрібен** %@!"; +"Common.RestartRequired" = "Потрібен перезапуск"; +"Common.RestartNow" = "Перезавантажити"; +"Common.OpenTelegram" = "Відкрити Telegram"; +"Common.UseTelegramForPremium" = "Зверніть увагу, що для отримання Telegram Premium ви маєте використовувати офіційний додаток Telegram. Після отримання Telegram Premium, усі переваги стануть доступними у Swiftgram."; + +"Message.HoldToShowOrReport" = "Затисніть, щоб переглянути або поскаржитись."; + +"Auth.AccountBackupReminder" = "Переконайтеся, що у вас є резервний метод доступу. Тримайте SIM-карту для SMS або додаткову сесію, щоб не втратити доступ до акаунту."; +"Auth.UnofficialAppCodeTitle" = "Ви можете отримати код тільки з офіційним додатком"; + +"Settings.SmallReactions" = "Малі реакції"; +"Settings.HideReactions" = "Приховувати реакції"; + +"ContextMenu.SaveToCloud" = "Переслати в Збережене"; +"ContextMenu.SelectFromUser" = "Вибрати від автора"; + +"Settings.ContextMenu" = "КОНТЕКСТНЕ МЕНЮ"; +"Settings.ContextMenu.Notice" = "Вимкнені елементи будуть доступні в підменю \"Swiftgram\"."; + + +"Settings.ChatSwipeOptions" = "Опції свайпу у списку чатів"; +"Settings.DeleteChatSwipeOption" = "Потягнути для видалення чату"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Потягнути до наступного каналу"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Потягнути до наступної гілки"; +"Settings.GalleryCamera" = "Камера в галереї"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Кнопка \"%@\""; +"Settings.SnapDeletionEffect" = "Ефекти видалення повідомлення"; + +"Settings.Stickers.Size" = "РОЗМІР"; +"Settings.Stickers.Timestamp" = "Показувати час"; + +"Settings.RecordingButton" = "Кнопка запису голосу"; + +"Settings.DefaultEmojisFirst" = "Пріоритизувати звичайні емодзі"; +"Settings.DefaultEmojisFirst.Notice" = "Показувати звичайні емодзі перед преміум у клавіатурі емодзі"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "створено: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Приєднався до %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Реєстрація"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Ред. повідомлення подвійним дотиком"; + +"Settings.wideChannelPosts" = "Широкі пости в каналах"; +"Settings.ForceEmojiTab" = "Клавіатура емодзі за замовчуванням"; + +"Settings.forceBuiltInMic" = "Використовувати мікрофон пристрою"; +"Settings.forceBuiltInMic.Notice" = "Якщо увімкнено, застосунок використовуватиме лише мікрофон пристрою, навіть якщо підключені навушники."; + +"Settings.hideChannelBottomButton" = "Приховати нижню панель каналів"; + +"Settings.CallConfirmation" = "Підтвердження викликів"; +"Settings.CallConfirmation.Notice" = "Swiftgram запитуватиме дозвіл перед здійсненням виклику."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Здійснити виклик?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Здійснити відеовиклик?"; + +"MutualContact.Label" = "взаємний контакт"; diff --git a/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..b0fba26068a --- /dev/null +++ b/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Kontent sozlamalari"; + +"Settings.Tabs.Header" = "Oynalar"; +"Settings.Tabs.HideTabBar" = "Oynalarni yashirish"; +"Settings.Tabs.ShowContacts" = "Kontaktlarni oynasini ko'rsatish"; +"Settings.Tabs.ShowNames" = "Oyna nomini ko'rsatish"; + +"Settings.Folders.BottomTab" = "Qurollar pastda"; +"Settings.Folders.BottomTabStyle" = "Pastki Qurollar uslubi"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iPhone"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "\"%@\"ni yashirish"; +"Settings.Folders.RememberLast" = "Oxirgi Jildni ochish"; +"Settings.Folders.RememberLast.Notice" = "Ilovani qayta ishga tushirganingizda yoki hisoblarni almashtirganingizda Swiftgram oxirgi foydalanilgan jildni ochadi."; + +"Settings.Folders.CompactNames" = "Kichik bo'sh joy"; +"Settings.Folders.AllChatsTitle" = "\"Barcha Chatlar\" nomi"; +"Settings.Folders.AllChatsTitle.short" = "Qisqa"; +"Settings.Folders.AllChatsTitle.long" = "Uzoq"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Standart"; + + +"Settings.ChatList.Header" = "CHAT RO'YXI"; +"Settings.CompactChatList" = "Qisqa Chat Ro'yxi"; + +"Settings.Profiles.Header" = "PROFILLAR"; + +"Settings.Stories.Hide" = "Hikoyalarni yashirish"; +"Settings.Stories.WarnBeforeView" = "Ko'rishdan avval tasdiqlash"; +"Settings.Stories.DisableSwipeToRecord" = "Kayd qilishni o'chirish"; + +"Settings.Translation.QuickTranslateButton" = "Tezkor tarjima tugmasi"; + +"Stories.Warning.Author" = "Muallif"; +"Stories.Warning.ViewStory" = "Hikoyani ko'rasizmi?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ hatto siz ularning Hikoyasini ko'rganini ko'rsatishadi."; +"Stories.Warning.NoticeStealth" = "%@ ularning Hikoyasini ko'rgani ko'rsatmaydi."; + +"Settings.Photo.Quality.Notice" = "Yuklanadigan fotosuratlar va hikoyalarning sifati."; +"Settings.Photo.SendLarge" = "Katta rasmlarni yuborish"; +"Settings.Photo.SendLarge.Notice" = "Tasodifiy rasmlarni to'g'rilangan hajmini 2560px ga oshiring."; + +"Settings.VideoNotes.Header" = "Aylana video"; +"Settings.VideoNotes.StartWithRearCam" = "Orqa kamerada boshlash"; + +"Settings.CustomColors.Header" = "Hisob ranglari"; +"Settings.CustomColors.Saturation" = "SATURATSIYA"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Hisob ranglarini o'chirish uchun 0%% ga sozlang."; + +"Settings.UploadsBoost" = "Yuklashni kuchaytirish"; +"Settings.DownloadsBoost" = "Yuklab olishni kuchaytirish"; +"Settings.DownloadsBoost.none" = "O'chirilgan"; +"Settings.DownloadsBoost.medium" = "O'rtacha"; +"Settings.DownloadsBoost.maximum" = "Maksimum"; + +"Settings.ShowProfileID" = "Profil Id'ni ko'rsatish"; +"Settings.ShowDC" = "Ma'lumotlar bazasini ko'rsatish"; +"Settings.ShowCreationDate" = "Suxbat yaratilgan sanani ko'rsatish"; +"Settings.ShowCreationDate.Notice" = "Ba'zi sahifalarning yaratilish sanasi ma'lum emas."; + +"Settings.ShowRegDate" = "Ro'yhatdan o'tish sanasini ko'rsatish"; +"Settings.ShowRegDate.Notice" = "Ro'yhatdan o'tgan sana yakunlanmagan."; + +"Settings.SendWithReturnKey" = "Enter orqali yuborish"; +"Settings.HidePhoneInSettingsUI" = "Telefonni sozlamalarda yashirish"; +"Settings.HidePhoneInSettingsUI.Notice" = "Bu faqat sozlamalardan telefon raqamingizni yashiradi. Uni boshqalar dan yashirish uchun, Farovonlik va Xavfsizlik ga o'ting."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "5 soniya uzoq bo'lsa"; + +"ProxySettings.UseSystemDNS" = "Tizim DNSni ishlat"; +"ProxySettings.UseSystemDNS.Notice" = "Agar sizda Google DNS guruhlaringiz bo'lmasa, istisnodan o'tish uchun tizim DNS ni ishlatishingiz kerak."; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Sizga %@ kerak emas!"; +"Common.RestartRequired" = "Qayta ishga tushirish lozim"; +"Common.RestartNow" = "Hozir qayta ishlash"; +"Common.OpenTelegram" = "Telegramni ochish"; +"Common.UseTelegramForPremium" = "Iltimos, Telegram Premiumni olish uchun rasmiy Telegram ilovasidan foydalaning. Telegram Premiumni olinganidan so'ng, barcha xususiyatlar Swiftgram da mavjud bo'ladi."; + +"Message.HoldToShowOrReport" = "Ko'rsatish yoki hisobga olish uchun tuting."; + +"Auth.AccountBackupReminder" = "Oldin saqlash usulini to'g'riroq o'rnatganingizni tekshiring. Alockli qilish uchun SMS uchun SIM kartni yoki qo'shimcha sessiyani tarqatib turish uchun qo'shimcha kirish usuliga kirish olib qo'ying."; +"Auth.UnofficialAppCodeTitle" = "Siz faqat rasmiy ilovadan faqat kodingizni olasiz"; + +"Settings.SmallReactions" = "Kichik Reaktsiyalar"; +"Settings.HideReactions" = "Reaksiyalarni yashirish"; + +"ContextMenu.SaveToCloud" = "Bulutga saqlash"; +"ContextMenu.SelectFromUser" = "Avtordan tanlash"; + +"Settings.ContextMenu" = "KONTEKS MENYU"; +"Settings.ContextMenu.Notice" = "O'chirilgan kirishlar \"Swiftgram\" pastki menudasiga o'tkaziladi."; + + +"Settings.ChatSwipeOptions" = "Chat Ro'yxati Sürüş variantlari"; +"Settings.DeleteChatSwipeOption" = "Sohbetni o'chirish uchun sug'urta"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Keyingi O'qilmagan Kanalga burilish"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Keyingi mavzuga torting"; +"Settings.GalleryCamera" = "Galereyadagi Kamera"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Tugma"; +"Settings.SnapDeletionEffect" = "Xabar O'chirish O'zgartirishlari"; + +"Settings.Stickers.Size" = "OLCHAM"; +"Settings.Stickers.Timestamp" = "Vaqtni Ko'rsatish"; + +"Settings.RecordingButton" = "Ovozni Yozish Tugmasi"; + +"Settings.DefaultEmojisFirst" = "Standart emoyilarni prioritetga qo'ying"; +"Settings.DefaultEmojisFirst.Notice" = "Emojilar klaviaturasida premiumdan oldin standart alifbo emoyilarni ko'rsating"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "yaratildi: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "%@\" ga qo'shildi"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Ro'yhatga olingan"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Xabarni tahrirlash uchun ikki marta bosing"; + +"Settings.wideChannelPosts" = "Keng postlar kanallarda"; +"Settings.ForceEmojiTab" = "Emoji klaviatura sukutiga"; + +"Settings.forceBuiltInMic" = "Qurilma Mikrofonini Kuchaytirish"; +"Settings.forceBuiltInMic.Notice" = "Agar yoqilsa, ilova faqat qurilma mikrofonidan foydalanadi, hattoki naushnik bog'langan bo'lsa ham."; + +"Settings.hideChannelBottomButton" = "Kanal Pastki Panellini yashirish"; diff --git a/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..e9b8521f6a3 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Cài đặt nội dung"; + +"Settings.Tabs.Header" = "THẺ"; +"Settings.Tabs.HideTabBar" = "Ẩn thanh Tab"; +"Settings.Tabs.ShowContacts" = "Hiện Liên hệ"; +"Settings.Tabs.ShowNames" = "Hiện tên các thẻ"; + +"Settings.Folders.BottomTab" = "Đặt thư mục tin nhắn ở dưới cùng"; +"Settings.Folders.BottomTabStyle" = "Kiểu Thư mục dưới cùng"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Ẩn \"%@\""; +"Settings.Folders.RememberLast" = "Mở thư mục gần đây"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram sẽ mở thư mục gần nhất sau khi khởi động lại hoặc chuyển tài khoản"; + +"Settings.Folders.CompactNames" = "Khoảng cách nhỏ hơn"; +"Settings.Folders.AllChatsTitle" = "Tiêu đề \"Tất cả Chat\""; +"Settings.Folders.AllChatsTitle.short" = "Ngắn"; +"Settings.Folders.AllChatsTitle.long" = "Dài"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Mặc định"; + + +"Settings.ChatList.Header" = "DANH SÁCH CHAT"; +"Settings.CompactChatList" = "Danh sách Chat Nhỏ gọn"; + +"Settings.Profiles.Header" = "HỒ SƠ"; + +"Settings.Stories.Hide" = "Ẩn Tin"; +"Settings.Stories.WarnBeforeView" = "Hỏi trước khi xem"; +"Settings.Stories.DisableSwipeToRecord" = "Tắt vuốt để quay"; + +"Settings.Translation.QuickTranslateButton" = "Hiện nút dịch nhanh"; + +"Stories.Warning.Author" = "Tác giả"; +"Stories.Warning.ViewStory" = "Xem Tin?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SẼ CÓ THỂ THẤY bạn đã xem Tin của họ."; +"Stories.Warning.NoticeStealth" = "%@ sẽ không biết bạn đã xem Tin của họ."; + +"Settings.Photo.Quality.Notice" = "Chất lượng của ảnh gửi đi và ảnh Tin"; +"Settings.Photo.SendLarge" = "Gửi ảnh lớn"; +"Settings.Photo.SendLarge.Notice" = "Tăng giới hạn kích thước bên trên của hình ảnh nén lên 2560px"; + +"Settings.VideoNotes.Header" = "VIDEO TRÒN"; +"Settings.VideoNotes.StartWithRearCam" = "Bắt đầu với camera sau"; + +"Settings.CustomColors.Header" = "MÀU TÀI KHOẢN"; +"Settings.CustomColors.Saturation" = "ĐỘ BÃO HÒA"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Đặt độ bão hòa thành 0%% để tắt màu tài khoản"; + +"Settings.UploadsBoost" = "Tăng tốc tải lên"; +"Settings.DownloadsBoost" = "Tăng tốc tải xuống"; +"Settings.DownloadsBoost.none" = "Tắt"; +"Settings.DownloadsBoost.medium" = "Trung bình"; +"Settings.DownloadsBoost.maximum" = "Tối đa"; + +"Settings.ShowProfileID" = "Hiện ID hồ sơ"; +"Settings.ShowDC" = "Hiển thị Trung tâm Dữ liệu"; +"Settings.ShowCreationDate" = "Hiển thị Ngày Tạo Chat"; +"Settings.ShowCreationDate.Notice" = "Ngày tạo có thể không biết được đối với một số cuộc trò chuyện."; + +"Settings.ShowRegDate" = "Hiển thị Ngày Đăng ký"; +"Settings.ShowRegDate.Notice" = "Ngày đăng ký là xấp xỉ."; + +"Settings.SendWithReturnKey" = "Gửi tín nhắn bằng nút \"Nhập\""; +"Settings.HidePhoneInSettingsUI" = "Ẩn số điện thoại trong cài đặt"; +"Settings.HidePhoneInSettingsUI.Notice" = "Số điện thoại của bạn sẽ chỉ ẩn đi trong cài đặt. Đến cài đặt \"Riêng tư và Bảo mật\" để ẩn đối với người khác\"."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Nếu rời đi trong 5 giây"; + +"ProxySettings.UseSystemDNS" = "Sử dụng DNS hệ thống"; +"ProxySettings.UseSystemDNS.Notice" = "Sử dụng DNS hệ thống để bỏ qua thời gian chờ nếu bạn không có quyền truy cập vào DNS của Google"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Bạn **không cần** %@!"; +"Common.RestartRequired" = "Yêu cầu khởi động lại"; +"Common.RestartNow" = "Khởi động lại"; +"Common.OpenTelegram" = "Mở Telegram"; +"Common.UseTelegramForPremium" = "Vui lòng lưu ý rằng để có được Telegram Premium, bạn phải sử dụng ứng dụng Telegram chính thức. Sau khi bạn đã có Telegram Premium, tất cả các tính năng của nó sẽ trở nên có sẵn trong Swiftgram."; + +"Message.HoldToShowOrReport" = "Nhấn giữ để Hiển thị hoặc Báo cáo."; + +"Auth.AccountBackupReminder" = "Hãy đảm bảo bạn có một phương pháp truy cập dự phòng. Giữ lại một SIM để nhận SMS hoặc một phiên đăng nhập bổ sung để tránh bị khóa tài khoản."; +"Auth.UnofficialAppCodeTitle" = "Bạn chỉ có thể nhận được mã thông qua ứng dụng chính thức"; + +"Settings.SmallReactions" = "Thu nhỏ biểu tượng cảm xúc"; +"Settings.HideReactions" = "Ẩn Biểu tượng cảm xúc"; + +"ContextMenu.SaveToCloud" = "Lưu vào Đám mây"; +"ContextMenu.SelectFromUser" = "Chọn từ Tác giả"; + +"Settings.ContextMenu" = "MENU NGỮ CẢNH"; +"Settings.ContextMenu.Notice" = "Mục nhập đã vô hiệu hóa sẽ có sẵn trong menu phụ 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "Tuỳ chọn Lướt Danh sách Chat"; +"Settings.DeleteChatSwipeOption" = "Vuốt để xóa Cuộc trò chuyện"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Kéo xuống đến kênh chưa đọc"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Kéo Để Đến Chủ Đề Tiếp Theo"; +"Settings.GalleryCamera" = "Máy ảnh trong thư viện"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" Nút"; +"Settings.SnapDeletionEffect" = "Hiệu Ứng Xóa Tin Nhắn"; + +"Settings.Stickers.Size" = "KÍCH THƯỚC"; +"Settings.Stickers.Timestamp" = "Hiện mốc thời gian"; + +"Settings.RecordingButton" = "Nút Ghi Âm Giọng Nói"; + +"Settings.DefaultEmojisFirst" = "Ưu tiên biểu tượng cảm xúc tiêu chuẩn"; +"Settings.DefaultEmojisFirst.Notice" = "Hiển thị biểu tượng cảm xúc tiêu chuẩn trước biểu tượng cảm xúc cao cấp trên bàn phím biểu tượng cảm xúc"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "đã tạo: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Đã tham gia %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Đã đăng ký"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Double-tap để chỉnh sửa tin nhắn"; + +"Settings.wideChannelPosts" = "Bài đăng rộng trong các kênh"; +"Settings.ForceEmojiTab" = "Bàn phím Emoji mặc định"; + +"Settings.forceBuiltInMic" = "Buộc Micro Điện Thoại"; +"Settings.forceBuiltInMic.Notice" = "Nếu được kích hoạt, ứng dụng sẽ chỉ sử dụng micro điện thoại của thiết bị ngay cả khi tai nghe được kết nối."; + +"Settings.hideChannelBottomButton" = "Ẩn thanh dưới cùng của kênh"; diff --git a/Swiftgram/SGStrings/Strings/zh-hans.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/zh-hans.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..287c4869d15 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/zh-hans.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "敏感内容设置"; + +"Settings.Tabs.Header" = "标签"; +"Settings.Tabs.HideTabBar" = "隐藏底部导航栏"; +"Settings.Tabs.ShowContacts" = "显示联系人标签"; +"Settings.Tabs.ShowNames" = "显示标签名称"; + +"Settings.Folders.BottomTab" = "底部分组"; +"Settings.Folders.BottomTabStyle" = "底部分组样式"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS样式"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram样式"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "隐藏 \"%@\""; +"Settings.Folders.RememberLast" = "打开上次分组"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram 将在重启或切换账户后打开最后使用的分组"; + +"Settings.Folders.CompactNames" = "缩小分组间距"; +"Settings.Folders.AllChatsTitle" = "\"所有对话\"标题"; +"Settings.Folders.AllChatsTitle.short" = "短标题"; +"Settings.Folders.AllChatsTitle.long" = "长标题"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "默认"; + + +"Settings.ChatList.Header" = "对话列表"; +"Settings.CompactChatList" = "紧凑型对话列表"; + +"Settings.Profiles.Header" = "资料"; + +"Settings.Stories.Hide" = "隐藏动态"; +"Settings.Stories.WarnBeforeView" = "查看前询问"; +"Settings.Stories.DisableSwipeToRecord" = "禁用侧滑拍摄"; + +"Settings.Translation.QuickTranslateButton" = "快速翻译按钮"; + +"Stories.Warning.Author" = "作者"; +"Stories.Warning.ViewStory" = "要查看动态吗?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ 将能够看到你查看了他们的动态"; +"Stories.Warning.NoticeStealth" = "%@ 将无法看到您查看他们的动态"; + +"Settings.Photo.Quality.Notice" = "发送图片的质量"; +"Settings.Photo.SendLarge" = "发送大尺寸照片"; +"Settings.Photo.SendLarge.Notice" = "将压缩图片的尺寸限制提高到 2560px"; + +"Settings.VideoNotes.Header" = "圆形视频"; +"Settings.VideoNotes.StartWithRearCam" = "默认使用后置相机"; + +"Settings.CustomColors.Header" = "账户颜色"; +"Settings.CustomColors.Saturation" = "饱和度"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "设置饱和度为 0%% 以禁用账户颜色"; + +"Settings.UploadsBoost" = "上传加速"; +"Settings.DownloadsBoost" = "下载加速"; +"Settings.DownloadsBoost.none" = "停用"; +"Settings.DownloadsBoost.medium" = "中等"; +"Settings.DownloadsBoost.maximum" = "最大"; + +"Settings.ShowProfileID" = "显示用户 UID"; +"Settings.ShowDC" = "显示数据中心"; +"Settings.ShowCreationDate" = "显示群组或频道的创建日期"; +"Settings.ShowCreationDate.Notice" = "某些群组或频道可能缺少创建日期"; + +"Settings.ShowRegDate" = "显示注册日期"; +"Settings.ShowRegDate.Notice" = "这是大概的注册日期"; + +"Settings.SendWithReturnKey" = "使用返回键发送"; +"Settings.HidePhoneInSettingsUI" = "在设置中隐藏电话号码"; +"Settings.HidePhoneInSettingsUI.Notice" = "您的电话号码只会在设置界面中隐藏。要对其他人隐藏,可进入隐私设置调整。"; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "离开 5 秒后"; + +"ProxySettings.UseSystemDNS" = "使用系统DNS"; +"ProxySettings.UseSystemDNS.Notice" = "如果您无法使用 Google DNS,请使用系统 DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "此功能**无需** %@ 订阅!"; +"Common.RestartRequired" = "需要重启"; +"Common.RestartNow" = "立即重启"; +"Common.OpenTelegram" = "打开 Telegram"; +"Common.UseTelegramForPremium" = "请注意,您必须使用官方的 Telegram 客户端才可购买 Telegram Premium,一旦您获得 Telegram Premium,其所有功能也将在 Swiftgram 中生效。"; + +"Message.HoldToShowOrReport" = "长按显示或举报"; + +"Auth.AccountBackupReminder" = "请确保您有一个备用的访问方式。保留一张用于接收短信的 SIM 卡或多登录一个会话,以免被锁定。"; +"Auth.UnofficialAppCodeTitle" = "您只能通过官方应用程序获得代码"; + +"Settings.SmallReactions" = "缩小表情回应"; +"Settings.HideReactions" = "隐藏回应"; + +"ContextMenu.SaveToCloud" = "保存到收藏夹"; +"ContextMenu.SelectFromUser" = "选择此人所有消息"; + +"Settings.ContextMenu" = "消息菜单"; +"Settings.ContextMenu.Notice" = "已禁用的项目可在 Swiftgram 子菜单中找到"; + + +"Settings.ChatSwipeOptions" = "对话列表滑动选项"; +"Settings.DeleteChatSwipeOption" = "滑动删除对话"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "上滑到下一未读频道"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "上滑到下一个主题"; +"Settings.GalleryCamera" = "图库中的相机"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" 按钮"; +"Settings.SnapDeletionEffect" = "删除消息的特效"; + +"Settings.Stickers.Size" = "尺寸"; +"Settings.Stickers.Timestamp" = "显示时间"; + +"Settings.RecordingButton" = "录音按钮"; + +"Settings.DefaultEmojisFirst" = "优先使用标准表情符号"; +"Settings.DefaultEmojisFirst.Notice" = "在表情列表中将标准表情符号置于高级表情符号之前"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "创建日期: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "加入 %@ 的日期"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "注册日期"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "双击编辑消息"; + +"Settings.wideChannelPosts" = "在频道中以更宽的版面显示消息"; +"Settings.ForceEmojiTab" = "默认展示表情符号"; + +"Settings.forceBuiltInMic" = "强制使用设备麦克风"; +"Settings.forceBuiltInMic.Notice" = "若启用,即使已连接耳机,应用也只使用设备自身的麦克风。"; + +"Settings.hideChannelBottomButton" = "隐藏频道底部面板"; diff --git a/Swiftgram/SGStrings/Strings/zh-hant.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/zh-hant.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..2c3804fdd86 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/zh-hant.lproj/SGLocalizable.strings @@ -0,0 +1,148 @@ +"Settings.ContentSettings" = "敏感內容設定"; + +"Settings.Tabs.Header" = "頁籤"; +"Settings.Tabs.HideTabBar" = "隱藏導航列"; +"Settings.Tabs.ShowContacts" = "顯示聯絡人頁籤"; +"Settings.Tabs.ShowNames" = "顯示頁籤名稱"; + +"Settings.Folders.BottomTab" = "底部頁籤"; +"Settings.Folders.BottomTabStyle" = "底部對話盒樣式"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "隱藏 \"%@\""; +"Settings.Folders.RememberLast" = "開啟最後瀏覽的對話盒"; +"Settings.Folders.RememberLast.Notice" = "當您重新啟動應用程式或切換帳戶時,Swiftgram 將開啟上次使用的對話盒。"; + +"Settings.Folders.CompactNames" = "縮小間距"; +"Settings.Folders.AllChatsTitle" = "\"所有對話\"標題"; +"Settings.Folders.AllChatsTitle.short" = "短"; +"Settings.Folders.AllChatsTitle.long" = "長"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "預設"; + + +"Settings.ChatList.Header" = "對話列表"; +"Settings.CompactChatList" = "緊湊型對話列表"; + +"Settings.Profiles.Header" = "配置文件"; + +"Settings.Stories.Hide" = "隱藏限時動態"; +"Settings.Stories.WarnBeforeView" = "瀏覽前確認"; +"Settings.Stories.DisableSwipeToRecord" = "停用滑動錄製"; + +"Settings.Translation.QuickTranslateButton" = "快速翻譯按鈕"; + +"Stories.Warning.Author" = "來自"; +"Stories.Warning.ViewStory" = "查看限時動態?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ 將會看到您瀏覽了限時動態。"; +"Stories.Warning.NoticeStealth" = "%@ 將無法看到您瀏覽了限時動態。"; + +"Settings.Photo.Quality.Notice" = "上傳影像的品質。"; +"Settings.Photo.SendLarge" = "傳送大尺寸影像"; +"Settings.Photo.SendLarge.Notice" = "將壓縮影像的尺寸限制增加到 2560 像素。"; + +"Settings.VideoNotes.Header" = "圓形影片"; +"Settings.VideoNotes.StartWithRearCam" = "預設使用後置鏡頭"; + +"Settings.CustomColors.Header" = "帳號顏色"; +"Settings.CustomColors.Saturation" = "飽和度"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "設定為 0%% 以停用帳戶顏色。"; + +"Settings.UploadsBoost" = "上傳加速"; +"Settings.DownloadsBoost" = "下載加速"; +"Settings.DownloadsBoost.none" = "已停用"; +"Settings.DownloadsBoost.medium" = "中等"; +"Settings.DownloadsBoost.maximum" = "最大"; + +"Settings.ShowProfileID" = "顯示用戶 UID"; +"Settings.ShowDC" = "顯示資料中心 (DC)"; +"Settings.ShowCreationDate" = "顯示對話建立日期"; +"Settings.ShowCreationDate.Notice" = "某些對話可能會缺少建立日期。"; + +"Settings.ShowRegDate" = "顯示註冊日期"; +"Settings.ShowRegDate.Notice" = "大約註冊日期"; + +"Settings.SendWithReturnKey" = "使用「換行」鍵傳送"; +"Settings.HidePhoneInSettingsUI" = "在設定頁中隱藏電話號碼"; +"Settings.HidePhoneInSettingsUI.Notice" = "您的電話在「設定頁」中不再顯示,可到「隱私與安全性」設定來對其他人隱藏。"; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "離開5秒後"; + +"ProxySettings.UseSystemDNS" = "使用系統 DNS"; +"ProxySettings.UseSystemDNS.Notice" = "如果您無法使用 Google DNS,請使用系統 DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "您 **不需要** %@!"; +"Common.RestartRequired" = "需要重新啟動"; +"Common.RestartNow" = "立即重啟"; +"Common.OpenTelegram" = "開啟 Telegram"; +"Common.UseTelegramForPremium" = "要獲得 Telegram Premium,您必須使用官方 Telegram App。一旦您擁有 Telegram Premium,其所有功能都將在 Swiftgram 中可用。"; + +"Message.HoldToShowOrReport" = "按住以顯示訊息或報告。"; + +"Auth.AccountBackupReminder" = "請確保您有備用訪問方法。保留用於接收簡訊的 SIM 卡或其他登入狀態以避免被鎖定。"; +"Auth.UnofficialAppCodeTitle" = "您只能透過官方 App 取得驗證碼"; + +"Settings.SmallReactions" = "縮小回應圖示"; +"Settings.HideReactions" = "隱藏回應"; + +"ContextMenu.SaveToCloud" = "轉傳到儲存的訊息"; +"ContextMenu.SelectFromUser" = "選取此人的所有訊息"; + +"Settings.ContextMenu" = "內容選單"; +"Settings.ContextMenu.Notice" = "停用的項目可在“Swiftgram”子選單中取得。"; + + +"Settings.ChatSwipeOptions" = "對話列表滑動選項"; +"Settings.DeleteChatSwipeOption" = "滑動刪除聊天記錄"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "頻道瀑布流"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "下拉以查看下一話題"; +"Settings.GalleryCamera" = "相簿圖庫"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "\"%@\" 按鈕"; +"Settings.SnapDeletionEffect" = "訊息刪除效果"; + +"Settings.Stickers.Size" = "尺寸"; +"Settings.Stickers.Timestamp" = "顯示時間戳"; + +"Settings.RecordingButton" = "錄音按鈕"; + +"Settings.DefaultEmojisFirst" = "優先顯示標準表情符號"; +"Settings.DefaultEmojisFirst.Notice" = "在表情符號鍵盤中,先顯示標準表情符號,再顯示 Premium 的"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "建立於:%@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "已加入 %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "註冊日期"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "雙擊以編輯訊息"; + +"Settings.wideChannelPosts" = "在頻道中以更寬的樣式顯示訊息"; +"Settings.ForceEmojiTab" = "預設表情符號鍵盤"; + +"Settings.forceBuiltInMic" = "強制使用裝置麥克風"; +"Settings.forceBuiltInMic.Notice" = "如果啟用,應用程式將只會使用設備麥克風。"; + +"Settings.hideChannelBottomButton" = "隱藏頻道底部面板"; + +"Settings.CallConfirmation" = "撥號確認"; +"Settings.CallConfirmation.Notice" = "Swiftgram 在撥打電話之前會要求您確認。"; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "打電話?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "進行視訊通話?"; + +"MutualContact.Label" = "雙向聯絡人"; diff --git a/Swiftgram/SGTranslationLangFix/BUILD b/Swiftgram/SGTranslationLangFix/BUILD new file mode 100644 index 00000000000..70f7354e971 --- /dev/null +++ b/Swiftgram/SGTranslationLangFix/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGTranslationLangFix", + module_name = "SGTranslationLangFix", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift b/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift new file mode 100644 index 00000000000..799daeac272 --- /dev/null +++ b/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift @@ -0,0 +1,7 @@ +public func sgTranslationLangFix(_ language: String) -> String { + if language.hasPrefix("zh-") { + return "zh" + } else { + return language + } +} \ No newline at end of file diff --git a/Swiftgram/SGWebAppExtensions/BUILD b/Swiftgram/SGWebAppExtensions/BUILD new file mode 100644 index 00000000000..1d581760f2d --- /dev/null +++ b/Swiftgram/SGWebAppExtensions/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebAppExtensions", + module_name = "SGWebAppExtensions", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift b/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift new file mode 100644 index 00000000000..355a5664c22 --- /dev/null +++ b/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift @@ -0,0 +1,58 @@ +import Foundation + +func urlSafeDecode(_ urlencoded: String) -> String { + return urlencoded.replacingOccurrences(of: "+", with: "%20").removingPercentEncoding ?? urlencoded +} + +public func urlParseHashParams(_ locationHash: String) -> [String: String?] { + var params = [String: String?]() + var localLocationHash = locationHash.removePrefix("#") // Remove leading '#' + + if localLocationHash.isEmpty { + return params + } + + if !localLocationHash.contains("=") && !localLocationHash.contains("?") { + params["_path"] = urlSafeDecode(localLocationHash) + return params + } + + let qIndex = localLocationHash.firstIndex(of: "?") + if let qIndex = qIndex { + let pathParam = String(localLocationHash[.. [String: String?] { + var params = [String: String?]() + + if queryString.isEmpty { + return params + } + + let queryStringParams = queryString.split(separator: "&") + for param in queryStringParams { + let parts = param.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + let paramName = urlSafeDecode(String(parts[0])) + let paramValue = parts.count > 1 ? urlSafeDecode(String(parts[1])) : nil + params[paramName] = paramValue + } + + return params +} + +extension String { + func removePrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } +} diff --git a/Swiftgram/SGWebSettings/BUILD b/Swiftgram/SGWebSettings/BUILD new file mode 100644 index 00000000000..ef1ee7626ad --- /dev/null +++ b/Swiftgram/SGWebSettings/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebSettings", + module_name = "SGWebSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebSettings/Sources/File.swift b/Swiftgram/SGWebSettings/Sources/File.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Swiftgram/SGWebSettingsScheme/BUILD b/Swiftgram/SGWebSettingsScheme/BUILD new file mode 100644 index 00000000000..7bec1071410 --- /dev/null +++ b/Swiftgram/SGWebSettingsScheme/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebSettingsScheme", + module_name = "SGWebSettingsScheme", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebSettingsScheme/Sources/File.swift b/Swiftgram/SGWebSettingsScheme/Sources/File.swift new file mode 100644 index 00000000000..b4545bedbb6 --- /dev/null +++ b/Swiftgram/SGWebSettingsScheme/Sources/File.swift @@ -0,0 +1,50 @@ +import Foundation + +public struct SGWebSettings: Codable, Equatable { + public let global: SGGlobalSettings + public let user: SGUserSettings + + public static var defaultValue: SGWebSettings { + return SGWebSettings(global: SGGlobalSettings(ytPip: true, qrLogin: true, storiesAvailable: false, canViewMessages: true, canEditSettings: false, canShowTelescope: false, announcementsData: nil, regdateFormat: "full", botMonkeys: [], forceReasons: [], unforceReasons: []), user: SGUserSettings(contentReasons: [], canSendTelescope: false)) + } +} + +public struct SGGlobalSettings: Codable, Equatable { + public let ytPip: Bool + public let qrLogin: Bool + public let storiesAvailable: Bool + public let canViewMessages: Bool + public let canEditSettings: Bool + public let canShowTelescope: Bool + public let announcementsData: String? + public let regdateFormat: String + public let botMonkeys: [SGBotMonkeys] + public let forceReasons: [Int64] + public let unforceReasons: [Int64] +} + +public struct SGBotMonkeys: Codable, Equatable { + public let botId: Int64 + public let src: String + public let enable: String + public let disable: String +} + + +public struct SGUserSettings: Codable, Equatable { + public let contentReasons: [String] + public let canSendTelescope: Bool +} + + +public extension SGUserSettings { + func expandedContentReasons() -> [String] { + return contentReasons.compactMap { base64String in + guard let data = Data(base64Encoded: base64String), + let decodedString = String(data: data, encoding: .utf8) else { + return nil + } + return decodedString + } + } +} diff --git a/Swiftgram/SwiftSoup/BUILD b/Swiftgram/SwiftSoup/BUILD new file mode 100644 index 00000000000..a4eeb901eac --- /dev/null +++ b/Swiftgram/SwiftSoup/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftSoup", + module_name = "SwiftSoup", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SwiftSoup/Sources/ArrayExt.swift b/Swiftgram/SwiftSoup/Sources/ArrayExt.swift new file mode 100644 index 00000000000..a3b329f03d6 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/ArrayExt.swift @@ -0,0 +1,21 @@ +// +// ArrayExt.swift +// SwifSoup +// +// Created by Nabil Chatbi on 05/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +extension Array where Element : Equatable { + func lastIndexOf(_ e: Element) -> Int { + for pos in (0.. String { + return key + } + + /** + Set the attribute key; case is preserved. + @param key the new key; must not be null + */ + open func setKey(key: String) throws { + try Validate.notEmpty(string: key) + self.key = key.trim() + } + + /** + Get the attribute value. + @return the attribute value + */ + open func getValue() -> String { + return value + } + + /** + Set the attribute value. + @param value the new attribute value; must not be null + */ + @discardableResult + open func setValue(value: String) -> String { + let old = self.value + self.value = value + return old + } + + /** + Get the HTML representation of this attribute; e.g. {@code href="index.html"}. + @return HTML + */ + public func html() -> String { + let accum = StringBuilder() + html(accum: accum, out: (Document("")).outputSettings()) + return accum.toString() + } + + public func html(accum: StringBuilder, out: OutputSettings ) { + accum.append(key) + if (!shouldCollapseAttribute(out: out)) { + accum.append("=\"") + Entities.escape(accum, value, out, true, false, false) + accum.append("\"") + } + } + + /** + Get the string representation of this attribute, implemented as {@link #html()}. + @return string + */ + open func toString() -> String { + return html() + } + + /** + * Create a new Attribute from an unencoded key and a HTML attribute encoded value. + * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars. + * @param encodedValue HTML attribute encoded value + * @return attribute + */ + public static func createFromEncoded(unencodedKey: String, encodedValue: String) throws ->Attribute { + let value = try Entities.unescape(string: encodedValue, strict: true) + return try Attribute(key: unencodedKey, value: value) + } + + public func isDataAttribute() -> Bool { + return key.startsWith(Attributes.dataPrefix) && key.count > Attributes.dataPrefix.count + } + + /** + * Collapsible if it's a boolean attribute and value is empty or same as name + * + * @param out Outputsettings + * @return Returns whether collapsible or not + */ + public final func shouldCollapseAttribute(out: OutputSettings) -> Bool { + return ("" == value || value.equalsIgnoreCase(string: key)) + && out.syntax() == OutputSettings.Syntax.html + && isBooleanAttribute() + } + + public func isBooleanAttribute() -> Bool { + return Attribute.booleanAttributes.contains(key.lowercased()) + } + + public func hashCode() -> Int { + var result = key.hashValue + result = 31 * result + value.hashValue + return result + } + + public func clone() -> Attribute { + do { + return try Attribute(key: key, value: value) + } catch Exception.Error( _, let msg) { + print(msg) + } catch { + + } + return try! Attribute(key: "", value: "") + } +} + +extension Attribute: Equatable { + static public func == (lhs: Attribute, rhs: Attribute) -> Bool { + return lhs.value == rhs.value && lhs.key == rhs.key + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/Attributes.swift b/Swiftgram/SwiftSoup/Sources/Attributes.swift new file mode 100644 index 00000000000..2ffa006a80b --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Attributes.swift @@ -0,0 +1,235 @@ +// +// Attributes.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * The attributes of an Element. + *

+ * Attributes are treated as a map: there can be only one value associated with an attribute key/name. + *

+ *

+ * Attribute name and value comparisons are case sensitive. By default for HTML, attribute names are + * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by + * name. + *

+ * + * + */ +open class Attributes: NSCopying { + + public static var dataPrefix: String = "data-" + + // Stored by lowercased key, but key case is checked against the copy inside + // the Attribute on retrieval. + var attributes: [Attribute] = [] + + public init() {} + + /** + Get an attribute value by key. + @param key the (case-sensitive) attribute key + @return the attribute value if set; or empty string if not set. + @see #hasKey(String) + */ + open func get(key: String) -> String { + if let attr = attributes.first(where: { $0.getKey() == key }) { + return attr.getValue() + } + return "" + } + + /** + * Get an attribute's value by case-insensitive key + * @param key the attribute name + * @return the first matching attribute value if set; or empty string if not set. + */ + open func getIgnoreCase(key: String )throws -> String { + try Validate.notEmpty(string: key) + if let attr = attributes.first(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame }) { + return attr.getValue() + } + return "" + } + + /** + Set a new attribute, or replace an existing one by key. + @param key attribute key + @param value attribute value + */ + open func put(_ key: String, _ value: String) throws { + let attr = try Attribute(key: key, value: value) + put(attribute: attr) + } + + /** + Set a new boolean attribute, remove attribute if value is false. + @param key attribute key + @param value attribute value + */ + open func put(_ key: String, _ value: Bool) throws { + if (value) { + try put(attribute: BooleanAttribute(key: key)) + } else { + try remove(key: key) + } + } + + /** + Set a new attribute, or replace an existing one by (case-sensitive) key. + @param attribute attribute + */ + open func put(attribute: Attribute) { + let key = attribute.getKey() + if let ix = attributes.firstIndex(where: { $0.getKey() == key }) { + attributes[ix] = attribute + } else { + attributes.append(attribute) + } + } + + /** + Remove an attribute by key. Case sensitive. + @param key attribute key to remove + */ + open func remove(key: String)throws { + try Validate.notEmpty(string: key) + if let ix = attributes.firstIndex(where: { $0.getKey() == key }) { + attributes.remove(at: ix) } + } + + /** + Remove an attribute by key. Case insensitive. + @param key attribute key to remove + */ + open func removeIgnoreCase(key: String ) throws { + try Validate.notEmpty(string: key) + if let ix = attributes.firstIndex(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame}) { + attributes.remove(at: ix) + } + } + + /** + Tests if these attributes contain an attribute with this key. + @param key case-sensitive key to check for + @return true if key exists, false otherwise + */ + open func hasKey(key: String) -> Bool { + return attributes.contains(where: { $0.getKey() == key }) + } + + /** + Tests if these attributes contain an attribute with this key. + @param key key to check for + @return true if key exists, false otherwise + */ + open func hasKeyIgnoreCase(key: String) -> Bool { + return attributes.contains(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame}) + } + + /** + Get the number of attributes in this set. + @return size + */ + open func size() -> Int { + return attributes.count + } + + /** + Add all the attributes from the incoming set to this set. + @param incoming attributes to add to these attributes. + */ + open func addAll(incoming: Attributes?) { + guard let incoming = incoming else { return } + for attr in incoming.attributes { + put(attribute: attr) + } + } + + /** + Get the attributes as a List, for iteration. Do not modify the keys of the attributes via this view, as changes + to keys will not be recognised in the containing set. + @return an view of the attributes as a List. + */ + open func asList() -> [Attribute] { + return attributes + } + + /** + * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys + * starting with {@code data-}. + * @return map of custom data attributes. + */ + open func dataset() -> [String: String] { + let prefixLength = Attributes.dataPrefix.count + let pairs = attributes.filter { $0.isDataAttribute() } + .map { ($0.getKey().substring(prefixLength), $0.getValue()) } + return Dictionary(uniqueKeysWithValues: pairs) + } + + /** + Get the HTML representation of these attributes. + @return HTML + @throws SerializationException if the HTML representation of the attributes cannot be constructed. + */ + open func html()throws -> String { + let accum = StringBuilder() + try html(accum: accum, out: Document("").outputSettings()) // output settings a bit funky, but this html() seldom used + return accum.toString() + } + + public func html(accum: StringBuilder, out: OutputSettings ) throws { + for attr in attributes { + accum.append(" ") + attr.html(accum: accum, out: out) + } + } + + open func toString()throws -> String { + return try html() + } + + /** + * Checks if these attributes are equal to another set of attributes, by comparing the two sets + * @param o attributes to compare with + * @return if both sets of attributes have the same content + */ + open func equals(o: AnyObject?) -> Bool { + if(o == nil) {return false} + if (self === o.self) {return true} + guard let that = o as? Attributes else {return false} + return (attributes == that.attributes) + } + + open func lowercaseAllKeys() { + for ix in attributes.indices { + attributes[ix].key = attributes[ix].key.lowercased() + } + } + + public func copy(with zone: NSZone? = nil) -> Any { + let clone = Attributes() + clone.attributes = attributes + return clone + } + + open func clone() -> Attributes { + return self.copy() as! Attributes + } + + fileprivate static func dataKey(key: String) -> String { + return dataPrefix + key + } + +} + +extension Attributes: Sequence { + public func makeIterator() -> AnyIterator { + return AnyIterator(attributes.makeIterator()) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/BinarySearch.swift b/Swiftgram/SwiftSoup/Sources/BinarySearch.swift new file mode 100644 index 00000000000..fb98c57701b --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/BinarySearch.swift @@ -0,0 +1,95 @@ +// +// BinarySearch.swift +// SwiftSoup-iOS +// +// Created by Garth Snyder on 2/28/19. +// Copyright © 2019 Nabil Chatbi. All rights reserved. +// +// Adapted from https://stackoverflow.com/questions/31904396/swift-binary-search-for-standard-array +// + +import Foundation + +extension Collection { + + /// Generalized binary search algorithm for ordered Collections + /// + /// Behavior is undefined if the collection is not properly sorted. + /// + /// This is only O(logN) for RandomAccessCollections; Collections in + /// general may implement offsetting of indexes as an O(K) operation. (E.g., + /// Strings are like this). + /// + /// - Note: If you are using this for searching only (not insertion), you + /// must always test the element at the returned index to ensure that + /// it's a genuine match. If the element is not present in the array, + /// you will still get a valid index back that represents the location + /// where it should be inserted. Also check to be sure the returned + /// index isn't off the end of the collection. + /// + /// - Parameter predicate: Reports the ordering of a given Element relative + /// to the desired Element. Typically, this is <. + /// + /// - Returns: Index N such that the predicate is true for all elements up to + /// but not including N, and is false for all elements N and beyond + + func binarySearch(predicate: (Element) -> Bool) -> Index { + var low = startIndex + var high = endIndex + while low != high { + let mid = index(low, offsetBy: distance(from: low, to: high)/2) + if predicate(self[mid]) { + low = index(after: mid) + } else { + high = mid + } + } + return low + } + + /// Binary search lookup for ordered Collections using a KeyPath + /// relative to Element. + /// + /// Behavior is undefined if the collection is not properly sorted. + /// + /// This is only O(logN) for RandomAccessCollections; Collections in + /// general may implement offsetting of indexes as an O(K) operation. (E.g., + /// Strings are like this). + /// + /// - Note: If you are using this for searching only (not insertion), you + /// must always test the element at the returned index to ensure that + /// it's a genuine match. If the element is not present in the array, + /// you will still get a valid index back that represents the location + /// where it should be inserted. Also check to be sure the returned + /// index isn't off the end of the collection. + /// + /// - Parameter keyPath: KeyPath that extracts the Element value on which + /// the Collection is presorted. Must be Comparable and Equatable. + /// ordering is presumed to be <, however that is defined for the type. + /// + /// - Returns: The index of a matching element, or nil if not found. If + /// the return value is non-nil, it is always a valid index. + + func indexOfElement(withValue value: T, atKeyPath keyPath: KeyPath) -> Index? where T: Comparable & Equatable { + let ix = binarySearch { $0[keyPath: keyPath] < value } + guard ix < endIndex else { return nil } + guard self[ix][keyPath: keyPath] == value else { return nil } + return ix + } + + func element(withValue value: T, atKeyPath keyPath: KeyPath) -> Element? where T: Comparable & Equatable { + if let ix = indexOfElement(withValue: value, atKeyPath: keyPath) { + return self[ix] + } + return nil + } + + func elements(withValue value: T, atKeyPath keyPath: KeyPath) -> [Element] where T: Comparable & Equatable { + guard let start = indexOfElement(withValue: value, atKeyPath: keyPath) else { return [] } + var end = index(after: start) + while end < endIndex && self[end][keyPath: keyPath] == value { + end = index(after: end) + } + return Array(self[start.. Bool { + return true + } +} diff --git a/Swiftgram/SwiftSoup/Sources/CharacterExt.swift b/Swiftgram/SwiftSoup/Sources/CharacterExt.swift new file mode 100644 index 00000000000..2cab2b56c70 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/CharacterExt.swift @@ -0,0 +1,81 @@ +// +// CharacterExt.swift +// SwifSoup +// +// Created by Nabil Chatbi on 08/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +extension Character { + + public static let space: Character = " " + public static let BackslashT: Character = "\t" + public static let BackslashN: Character = "\n" + public static let BackslashF: Character = Character(UnicodeScalar(12)) + public static let BackslashR: Character = "\r" + public static let BackshashRBackslashN: Character = "\r\n" + + //http://www.unicode.org/glossary/#supplementary_code_point + public static let MIN_SUPPLEMENTARY_CODE_POINT: UInt32 = 0x010000 + + /// True for any space character, and the control characters \t, \n, \r, \f, \v. + + var isWhitespace: Bool { + switch self { + case Character.space, Character.BackslashT, Character.BackslashN, Character.BackslashF, Character.BackslashR: return true + case Character.BackshashRBackslashN: return true + default: return false + + } + } + + /// `true` if `self` normalized contains a single code unit that is in the category of Decimal Numbers. + var isDigit: Bool { + + return isMemberOfCharacterSet(CharacterSet.decimalDigits) + + } + + /// Lowercase `self`. + var lowercase: Character { + + let str = String(self).lowercased() + return str[str.startIndex] + + } + + /// Return `true` if `self` normalized contains a single code unit that is a member of the supplied character set. + /// + /// - parameter set: The `NSCharacterSet` used to test for membership. + /// - returns: `true` if `self` normalized contains a single code unit that is a member of the supplied character set. + func isMemberOfCharacterSet(_ set: CharacterSet) -> Bool { + + let normalized = String(self).precomposedStringWithCanonicalMapping + let unicodes = normalized.unicodeScalars + + guard unicodes.count == 1 else { return false } + return set.contains(UnicodeScalar(unicodes.first!.value)!) + + } + + static func convertFromIntegerLiteral(value: IntegerLiteralType) -> Character { + return Character(UnicodeScalar(value)!) + } + + static func isLetter(_ char: Character) -> Bool { + return char.isLetter() + } + func isLetter() -> Bool { + return self.isMemberOfCharacterSet(CharacterSet.letters) + } + + static func isLetterOrDigit(_ char: Character) -> Bool { + return char.isLetterOrDigit() + } + func isLetterOrDigit() -> Bool { + if(self.isLetter()) {return true} + return self.isDigit + } +} diff --git a/Swiftgram/SwiftSoup/Sources/CharacterReader.swift b/Swiftgram/SwiftSoup/Sources/CharacterReader.swift new file mode 100644 index 00000000000..d53c7950720 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/CharacterReader.swift @@ -0,0 +1,320 @@ +// +// CharacterReader.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 10/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + CharacterReader consumes tokens off a string. To replace the old TokenQueue. + */ +public final class CharacterReader { + private static let empty = "" + public static let EOF: UnicodeScalar = "\u{FFFF}"//65535 + private let input: String.UnicodeScalarView + private var pos: String.UnicodeScalarView.Index + private var mark: String.UnicodeScalarView.Index + //private let stringCache: Array // holds reused strings in this doc, to lessen garbage + + public init(_ input: String) { + self.input = input.unicodeScalars + self.pos = input.startIndex + self.mark = input.startIndex + } + + public func getPos() -> Int { + return input.distance(from: input.startIndex, to: pos) + } + + public func isEmpty() -> Bool { + return pos >= input.endIndex + } + + public func current() -> UnicodeScalar { + return (pos >= input.endIndex) ? CharacterReader.EOF : input[pos] + } + + @discardableResult + public func consume() -> UnicodeScalar { + guard pos < input.endIndex else { + return CharacterReader.EOF + } + let val = input[pos] + pos = input.index(after: pos) + return val + } + + public func unconsume() { + guard pos > input.startIndex else { return } + pos = input.index(before: pos) + } + + public func advance() { + guard pos < input.endIndex else { return } + pos = input.index(after: pos) + } + + public func markPos() { + mark = pos + } + + public func rewindToMark() { + pos = mark + } + + public func consumeAsString() -> String { + guard pos < input.endIndex else { return "" } + let str = String(input[pos]) + pos = input.index(after: pos) + return str + } + + /** + * Locate the next occurrence of a Unicode scalar + * + * - Parameter c: scan target + * - Returns: offset between current position and next instance of target. -1 if not found. + */ + public func nextIndexOf(_ c: UnicodeScalar) -> String.UnicodeScalarView.Index? { + // doesn't handle scanning for surrogates + return input[pos...].firstIndex(of: c) + } + + /** + * Locate the next occurence of a target string + * + * - Parameter seq: scan target + * - Returns: index of next instance of target. nil if not found. + */ + public func nextIndexOf(_ seq: String) -> String.UnicodeScalarView.Index? { + // doesn't handle scanning for surrogates + var start = pos + let targetScalars = seq.unicodeScalars + guard let firstChar = targetScalars.first else { return pos } // search for "" -> current place + MATCH: while true { + // Match on first scalar + guard let firstCharIx = input[start...].firstIndex(of: firstChar) else { return nil } + var current = firstCharIx + // Then manually match subsequent scalars + for scalar in targetScalars.dropFirst() { + current = input.index(after: current) + guard current < input.endIndex else { return nil } + if input[current] != scalar { + start = input.index(after: firstCharIx) + continue MATCH + } + } + // full match; current is at position of last matching character + return firstCharIx + } + } + + public func consumeTo(_ c: UnicodeScalar) -> String { + guard let targetIx = nextIndexOf(c) else { + return consumeToEnd() + } + let consumed = cacheString(pos, targetIx) + pos = targetIx + return consumed + } + + public func consumeTo(_ seq: String) -> String { + guard let targetIx = nextIndexOf(seq) else { + return consumeToEnd() + } + let consumed = cacheString(pos, targetIx) + pos = targetIx + return consumed + } + + public func consumeToAny(_ chars: UnicodeScalar...) -> String { + return consumeToAny(chars) + } + + public func consumeToAny(_ chars: [UnicodeScalar]) -> String { + let start = pos + while pos < input.endIndex { + if chars.contains(input[pos]) { + break + } + pos = input.index(after: pos) + } + return cacheString(start, pos) + } + + public func consumeToAnySorted(_ chars: UnicodeScalar...) -> String { + return consumeToAny(chars) + } + + public func consumeToAnySorted(_ chars: [UnicodeScalar]) -> String { + return consumeToAny(chars) + } + + static let dataTerminators: [UnicodeScalar] = [.Ampersand, .LessThan, TokeniserStateVars.nullScalr] + // read to &, <, or null + public func consumeData() -> String { + return consumeToAny(CharacterReader.dataTerminators) + } + + static let tagNameTerminators: [UnicodeScalar] = [.BackslashT, .BackslashN, .BackslashR, .BackslashF, .Space, .Slash, .GreaterThan, TokeniserStateVars.nullScalr] + // read to '\t', '\n', '\r', '\f', ' ', '/', '>', or nullChar + public func consumeTagName() -> String { + return consumeToAny(CharacterReader.tagNameTerminators) + } + + public func consumeToEnd() -> String { + let consumed = cacheString(pos, input.endIndex) + pos = input.endIndex + return consumed + } + + public func consumeLetterSequence() -> String { + let start = pos + while pos < input.endIndex { + let c = input[pos] + if ((c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters)) { + pos = input.index(after: pos) + } else { + break + } + } + return cacheString(start, pos) + } + + public func consumeLetterThenDigitSequence() -> String { + let start = pos + while pos < input.endIndex { + let c = input[pos] + if ((c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters)) { + pos = input.index(after: pos) + } else { + break + } + } + while pos < input.endIndex { + let c = input[pos] + if (c >= "0" && c <= "9") { + pos = input.index(after: pos) + } else { + break + } + } + return cacheString(start, pos) + } + + public func consumeHexSequence() -> String { + let start = pos + while pos < input.endIndex { + let c = input[pos] + if ((c >= "0" && c <= "9") || (c >= "A" && c <= "F") || (c >= "a" && c <= "f")) { + pos = input.index(after: pos) + } else { + break + } + } + return cacheString(start, pos) + } + + public func consumeDigitSequence() -> String { + let start = pos + while pos < input.endIndex { + let c = input[pos] + if (c >= "0" && c <= "9") { + pos = input.index(after: pos) + } else { + break + } + } + return cacheString(start, pos) + } + + public func matches(_ c: UnicodeScalar) -> Bool { + return !isEmpty() && input[pos] == c + + } + + public func matches(_ seq: String, ignoreCase: Bool = false, consume: Bool = false) -> Bool { + var current = pos + let scalars = seq.unicodeScalars + for scalar in scalars { + guard current < input.endIndex else { return false } + if ignoreCase { + guard input[current].uppercase == scalar.uppercase else { return false } + } else { + guard input[current] == scalar else { return false } + } + current = input.index(after: current) + } + if consume { + pos = current + } + return true + } + + public func matchesIgnoreCase(_ seq: String ) -> Bool { + return matches(seq, ignoreCase: true) + } + + public func matchesAny(_ seq: UnicodeScalar...) -> Bool { + return matchesAny(seq) + } + + public func matchesAny(_ seq: [UnicodeScalar]) -> Bool { + guard pos < input.endIndex else { return false } + return seq.contains(input[pos]) + } + + public func matchesAnySorted(_ seq: [UnicodeScalar]) -> Bool { + return matchesAny(seq) + } + + public func matchesLetter() -> Bool { + guard pos < input.endIndex else { return false } + let c = input[pos] + return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters) + } + + public func matchesDigit() -> Bool { + guard pos < input.endIndex else { return false } + let c = input[pos] + return c >= "0" && c <= "9" + } + + @discardableResult + public func matchConsume(_ seq: String) -> Bool { + return matches(seq, consume: true) + } + + @discardableResult + public func matchConsumeIgnoreCase(_ seq: String) -> Bool { + return matches(seq, ignoreCase: true, consume: true) + } + + public func containsIgnoreCase(_ seq: String ) -> Bool { + // used to check presence of , . only finds consistent case. + let loScan = seq.lowercased(with: Locale(identifier: "en")) + let hiScan = seq.uppercased(with: Locale(identifier: "eng")) + return nextIndexOf(loScan) != nil || nextIndexOf(hiScan) != nil + } + + public func toString() -> String { + return String(input[pos...]) + } + + /** + * Originally intended as a caching mechanism for strings, but caching doesn't + * seem to improve performance. Now just a stub. + */ + private func cacheString(_ start: String.UnicodeScalarView.Index, _ end: String.UnicodeScalarView.Index) -> String { + return String(input[start..` and `` using the supplied whitelist. + /// - Parameters: + /// - headWhitelist: Whitelist to clean the head with + /// - bodyWhitelist: Whitelist to clean the body with + public init(headWhitelist: Whitelist?, bodyWhitelist: Whitelist) { + self.headWhitelist = headWhitelist + self.bodyWhitelist = bodyWhitelist + } + + /// Create a new cleaner, that sanitizes documents' `` using the supplied whitelist. + /// - Parameter whitelist: Whitelist to clean the body with + convenience init(_ whitelist: Whitelist) { + self.init(headWhitelist: nil, bodyWhitelist: whitelist) + } + + /// Creates a new, clean document, from the original dirty document, containing only elements allowed by the whitelist. + /// The original document is not modified. Only elements from the dirt document's `` are used. + /// - Parameter dirtyDocument: Untrusted base document to clean. + /// - Returns: A cleaned document. + public func clean(_ dirtyDocument: Document) throws -> Document { + let clean = Document.createShell(dirtyDocument.getBaseUri()) + if let headWhitelist, let dirtHead = dirtyDocument.head(), let cleanHead = clean.head() { // frameset documents won't have a head. the clean doc will have empty head. + try copySafeNodes(dirtHead, cleanHead, whitelist: headWhitelist) + } + if let dirtBody = dirtyDocument.body(), let cleanBody = clean.body() { // frameset documents won't have a body. the clean doc will have empty body. + try copySafeNodes(dirtBody, cleanBody, whitelist: bodyWhitelist) + } + return clean + } + + /// Determines if the input document is valid, against the whitelist. It is considered valid if all the tags and attributes + /// in the input HTML are allowed by the whitelist. + /// + /// This method can be used as a validator for user input forms. An invalid document will still be cleaned successfully + /// using the ``clean(_:)`` document. If using as a validator, it is recommended to still clean the document + /// to ensure enforced attributes are set correctly, and that the output is tidied. + /// - Parameter dirtyDocument: document to test + /// - Returns: true if no tags or attributes need to be removed; false if they do + public func isValid(_ dirtyDocument: Document) throws -> Bool { + let clean = Document.createShell(dirtyDocument.getBaseUri()) + let numDiscarded = try copySafeNodes(dirtyDocument.body()!, clean.body()!, whitelist: bodyWhitelist) + return numDiscarded == 0 + } + + @discardableResult + fileprivate func copySafeNodes(_ source: Element, _ dest: Element, whitelist: Whitelist) throws -> Int { + let cleaningVisitor = Cleaner.CleaningVisitor(source, dest, whitelist) + try NodeTraversor(cleaningVisitor).traverse(source) + return cleaningVisitor.numDiscarded + } +} + +extension Cleaner { + fileprivate final class CleaningVisitor: NodeVisitor { + private(set) var numDiscarded = 0 + + private let root: Element + private var destination: Element? // current element to append nodes to + + private let whitelist: Whitelist + + public init(_ root: Element, _ destination: Element, _ whitelist: Whitelist) { + self.root = root + self.destination = destination + self.whitelist = whitelist + } + + public func head(_ source: Node, _ depth: Int) throws { + if let sourceEl = source as? Element { + if whitelist.isSafeTag(sourceEl.tagName()) { // safe, clone and copy safe attrs + let meta = try createSafeElement(sourceEl) + let destChild = meta.el + try destination?.appendChild(destChild) + + numDiscarded += meta.numAttribsDiscarded + destination = destChild + } else if source != root { // not a safe tag, so don't add. don't count root against discarded. + numDiscarded += 1 + } + } else if let sourceText = source as? TextNode { + let destText = TextNode(sourceText.getWholeText(), source.getBaseUri()) + try destination?.appendChild(destText) + } else if let sourceData = source as? DataNode { + if sourceData.parent() != nil && whitelist.isSafeTag(sourceData.parent()!.nodeName()) { + let destData = DataNode(sourceData.getWholeData(), source.getBaseUri()) + try destination?.appendChild(destData) + } else { + numDiscarded += 1 + } + } else { // else, we don't care about comments, xml proc instructions, etc + numDiscarded += 1 + } + } + + public func tail(_ source: Node, _ depth: Int) throws { + if let x = source as? Element { + if whitelist.isSafeTag(x.nodeName()) { + // would have descended, so pop destination stack + destination = destination?.parent() + } + } + } + + private func createSafeElement(_ sourceEl: Element) throws -> ElementMeta { + let sourceTag = sourceEl.tagName() + let destAttrs = Attributes() + var numDiscarded = 0 + + if let sourceAttrs = sourceEl.getAttributes() { + for sourceAttr in sourceAttrs { + if try whitelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr) { + destAttrs.put(attribute: sourceAttr) + } else { + numDiscarded += 1 + } + } + } + let enforcedAttrs = try whitelist.getEnforcedAttributes(sourceTag) + destAttrs.addAll(incoming: enforcedAttrs) + + let dest = try Element(Tag.valueOf(sourceTag), sourceEl.getBaseUri(), destAttrs) + return ElementMeta(dest, numDiscarded) + } + } +} + +extension Cleaner { + fileprivate struct ElementMeta { + let el: Element + let numAttribsDiscarded: Int + + init(_ el: Element, _ numAttribsDiscarded: Int) { + self.el = el + self.numAttribsDiscarded = numAttribsDiscarded + } + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Collector.swift b/Swiftgram/SwiftSoup/Sources/Collector.swift new file mode 100644 index 00000000000..7bb6feb5929 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Collector.swift @@ -0,0 +1,59 @@ +// +// Collector.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 22/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * Collects a list of elements that match the supplied criteria. + * + */ +open class Collector { + + private init() { + } + + /** + Build a list of elements, by visiting root and every descendant of root, and testing it against the evaluator. + @param eval Evaluator to test elements against + @param root root of tree to descend + @return list of matches; empty if none + */ + public static func collect (_ eval: Evaluator, _ root: Element)throws->Elements { + let elements: Elements = Elements() + try NodeTraversor(Accumulator(root, elements, eval)).traverse(root) + return elements + } + +} + +private final class Accumulator: NodeVisitor { + private let root: Element + private let elements: Elements + private let eval: Evaluator + + init(_ root: Element, _ elements: Elements, _ eval: Evaluator) { + self.root = root + self.elements = elements + self.eval = eval + } + + public func head(_ node: Node, _ depth: Int) { + guard let el = node as? Element else { + return + } + do { + if try eval.matches(root, el) { + elements.add(el) + } + } catch {} + } + + public func tail(_ node: Node, _ depth: Int) { + // void + } +} diff --git a/Swiftgram/SwiftSoup/Sources/CombiningEvaluator.swift b/Swiftgram/SwiftSoup/Sources/CombiningEvaluator.swift new file mode 100644 index 00000000000..fdeb0aebbe2 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/CombiningEvaluator.swift @@ -0,0 +1,127 @@ +// +// CombiningEvaluator.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 23/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * Base combining (and, or) evaluator. + */ +public class CombiningEvaluator: Evaluator { + + public private(set) var evaluators: Array + var num: Int = 0 + + public override init() { + evaluators = Array() + super.init() + } + + public init(_ evaluators: Array) { + self.evaluators = evaluators + super.init() + updateNumEvaluators() + } + + public init(_ evaluators: Evaluator...) { + self.evaluators = evaluators + super.init() + updateNumEvaluators() + } + + func rightMostEvaluator() -> Evaluator? { + return num > 0 && evaluators.count > 0 ? evaluators[num - 1] : nil + } + + func replaceRightMostEvaluator(_ replacement: Evaluator) { + evaluators[num - 1] = replacement + } + + func updateNumEvaluators() { + // used so we don't need to bash on size() for every match test + num = evaluators.count + } + + public final class And: CombiningEvaluator { + public override init(_ evaluators: [Evaluator]) { + super.init(evaluators) + } + + public override init(_ evaluators: Evaluator...) { + super.init(evaluators) + } + + public override func matches(_ root: Element, _ node: Element) -> Bool { + for index in 0.. String { + let array: [String] = evaluators.map { String($0.toString()) } + return StringUtil.join(array, sep: " ") + } + } + + public final class Or: CombiningEvaluator { + /** + * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR. + * @param evaluators initial OR clause (these are wrapped into an AND evaluator). + */ + public override init(_ evaluators: [Evaluator]) { + super.init() + if num > 1 { + self.evaluators.append(And(evaluators)) + } else { // 0 or 1 + self.evaluators.append(contentsOf: evaluators) + } + updateNumEvaluators() + } + + override init(_ evaluators: Evaluator...) { + super.init() + if num > 1 { + self.evaluators.append(And(evaluators)) + } else { // 0 or 1 + self.evaluators.append(contentsOf: evaluators) + } + updateNumEvaluators() + } + + override init() { + super.init() + } + + public func add(_ evaluator: Evaluator) { + evaluators.append(evaluator) + updateNumEvaluators() + } + + public override func matches(_ root: Element, _ node: Element) -> Bool { + for index in 0.. String { + return ":or\(evaluators.map {String($0.toString())})" + } + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Comment.swift b/Swiftgram/SwiftSoup/Sources/Comment.swift new file mode 100644 index 00000000000..0892cad3fad --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Comment.swift @@ -0,0 +1,66 @@ +// +// Comment.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 22/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + A comment node. + */ +public class Comment: Node { + private static let COMMENT_KEY: String = "comment" + + /** + Create a new comment node. + @param data The contents of the comment + @param baseUri base URI + */ + public init(_ data: String, _ baseUri: String) { + super.init(baseUri) + do { + try attributes?.put(Comment.COMMENT_KEY, data) + } catch {} + } + + public override func nodeName() -> String { + return "#comment" + } + + /** + Get the contents of the comment. + @return comment content + */ + public func getData() -> String { + return attributes!.get(key: Comment.COMMENT_KEY) + } + + override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { + if (out.prettyPrint()) { + indent(accum, depth, out) + } + accum + .append("") + } + + override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {} + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = Comment(attributes!.get(key: Comment.COMMENT_KEY), baseUri!) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = Comment(attributes!.get(key: Comment.COMMENT_KEY), baseUri!) + return copy(clone: clone, parent: parent) + } + + public override func copy(clone: Node, parent: Node?) -> Node { + return super.copy(clone: clone, parent: parent) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Connection.swift b/Swiftgram/SwiftSoup/Sources/Connection.swift new file mode 100644 index 00000000000..7b309a53c54 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Connection.swift @@ -0,0 +1,10 @@ +// +// Connection.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation +//TODO: diff --git a/Swiftgram/SwiftSoup/Sources/CssSelector.swift b/Swiftgram/SwiftSoup/Sources/CssSelector.swift new file mode 100644 index 00000000000..c8129220e8d --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/CssSelector.swift @@ -0,0 +1,166 @@ +// +// CssSelector.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 21/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * CSS-like element selector, that finds elements matching a query. + * + *

CssSelector syntax

+ *

+ * A selector is a chain of simple selectors, separated by combinators. Selectors are case insensitive (including against + * elements, attributes, and attribute values). + *

+ *

+ * The universal selector (*) is implicit when no element selector is supplied (i.e. {@code *.header} and {@code .header} + * is equivalent). + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PatternMatchesExample
*any element*
tagelements with the given tag namediv
*|Eelements of type E in any namespace ns*|name finds <fb:name> elements
ns|Eelements of type E in the namespace nsfb|name finds <fb:name> elements
#idelements with attribute ID of "id"div#wrap, #logo
.classelements with a class name of "class"div.left, .result
[attr]elements with an attribute named "attr" (with any value)a[href], [title]
[^attrPrefix]elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets[^data-], div[^data-]
[attr=val]elements with an attribute named "attr", and value equal to "val"img[width=500], a[rel=nofollow]
[attr="val"]elements with an attribute named "attr", and value equal to "val"span[hello="Cleveland"][goodbye="Columbus"], a[rel="nofollow"]
[attr^=valPrefix]elements with an attribute named "attr", and value starting with "valPrefix"a[href^=http:]
[attr$=valSuffix]elements with an attribute named "attr", and value ending with "valSuffix"img[src$=.png]
[attr*=valContaining]elements with an attribute named "attr", and value containing "valContaining"a[href*=/search/]
[attr~=regex]elements with an attribute named "attr", and value matching the regular expressionimg[src~=(?i)\\.(png|jpe?g)]
The above may be combined in any orderdiv.header[title]

Combinators

E Fan F element descended from an E elementdiv a, .logo h1
E {@literal >} Fan F direct child of Eol {@literal >} li
E + Fan F element immediately preceded by sibling Eli + li, div.head + div
E ~ Fan F element preceded by sibling Eh1 ~ p
E, F, Gall matching elements E, F, or Ga[href], div, h3

Pseudo selectors

:lt(n)elements whose sibling index is less than ntd:lt(3) finds the first 3 cells of each row
:gt(n)elements whose sibling index is greater than ntd:gt(1) finds cells after skipping the first two
:eq(n)elements whose sibling index is equal to ntd:eq(0) finds the first cell of each row
:has(selector)elements that contains at least one element matching the selectordiv:has(p) finds divs that contain p elements
:not(selector)elements that do not match the selector. See also {@link Elements#not(String)}div:not(.logo) finds all divs that do not have the "logo" class.

div:not(:has(div)) finds divs that do not contain divs.

:contains(text)elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.p:contains(SwiftSoup) finds p elements containing the text "SwiftSoup".
:matches(regex)elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.td:matches(\\d+) finds table cells containing digits. div:matches((?i)login) finds divs containing the text, case insensitively.
:containsOwn(text)elements that directly contain the specified text. The search is case insensitive. The text must appear in the found element, not any of its descendants.p:containsOwn(SwiftSoup) finds p elements with own text "SwiftSoup".
:matchesOwn(regex)elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants.td:matchesOwn(\\d+) finds table cells directly containing digits. div:matchesOwn((?i)login) finds divs containing the text, case insensitively.
The above may be combined in any order and with other selectors.light:contains(name):eq(0)

Structural pseudo selectors

:rootThe element that is the root of the document. In HTML, this is the html element:root
:nth-child(an+b)

elements that have an+b-1 siblings before it in the document tree, for any positive integer or zero value of n, and has a parent element. For values of a and b greater than zero, this effectively divides the element's children into groups of a elements (the last group taking the remainder), and selecting the bth element of each group. For example, this allows the selectors to address every other row in a table, and could be used to alternate the color of paragraph text in a cycle of four. The a and b values must be integers (positive, negative, or zero). The index of the first child of an element is 1.

+ * In addition to this, :nth-child() can take odd and even as arguments instead. odd has the same signification as 2n+1, and even has the same signification as 2n.
tr:nth-child(2n+1) finds every odd row of a table. :nth-child(10n-1) the 9th, 19th, 29th, etc, element. li:nth-child(5) the 5h li
:nth-last-child(an+b)elements that have an+b-1 siblings after it in the document tree. Otherwise like :nth-child()tr:nth-last-child(-n+2) the last two rows of a table
:nth-of-type(an+b)pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name before it in the document tree, for any zero or positive integer value of n, and has a parent elementimg:nth-of-type(2n+1)
:nth-last-of-type(an+b)pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name after it in the document tree, for any zero or positive integer value of n, and has a parent elementimg:nth-last-of-type(2n+1)
:first-childelements that are the first child of some other element.div {@literal >} p:first-child
:last-childelements that are the last child of some other element.ol {@literal >} li:last-child
:first-of-typeelements that are the first sibling of its type in the list of children of its parent elementdl dt:first-of-type
:last-of-typeelements that are the last sibling of its type in the list of children of its parent elementtr {@literal >} td:last-of-type
:only-childelements that have a parent element and whose parent element hasve no other element children
:only-of-type an element that has a parent element and whose parent element has no other element children with the same expanded element name
:emptyelements that have no children at all
+ * + * @see Element#select(String) + */ +@available(*, deprecated, renamed: "CssSelector") +typealias Selector = CssSelector + +open class CssSelector { + private let evaluator: Evaluator + private let root: Element + + private init(_ query: String, _ root: Element)throws { + let query = query.trim() + try Validate.notEmpty(string: query) + + self.evaluator = try QueryParser.parse(query) + + self.root = root + } + + private init(_ evaluator: Evaluator, _ root: Element) { + self.evaluator = evaluator + self.root = root + } + + /** + * Find elements matching selector. + * + * @param query CSS selector + * @param root root element to descend into + * @return matching elements, empty if none + * @throws CssSelector.SelectorParseException (unchecked) on an invalid CSS query. + */ + public static func select(_ query: String, _ root: Element)throws->Elements { + return try CssSelector(query, root).select() + } + + /** + * Find elements matching selector. + * + * @param evaluator CSS selector + * @param root root element to descend into + * @return matching elements, empty if none + */ + public static func select(_ evaluator: Evaluator, _ root: Element)throws->Elements { + return try CssSelector(evaluator, root).select() + } + + /** + * Find elements matching selector. + * + * @param query CSS selector + * @param roots root elements to descend into + * @return matching elements, empty if none + */ + public static func select(_ query: String, _ roots: Array)throws->Elements { + try Validate.notEmpty(string: query) + let evaluator: Evaluator = try QueryParser.parse(query) + var elements: Array = Array() + var seenElements: Array = Array() + // dedupe elements by identity, not equality + + for root: Element in roots { + let found: Elements = try select(evaluator, root) + for el: Element in found.array() { + if (!seenElements.contains(el)) { + elements.append(el) + seenElements.append(el) + } + } + } + return Elements(elements) + } + + private func select()throws->Elements { + return try Collector.collect(evaluator, root) + } + + // exclude set. package open so that Elements can implement .not() selector. + static func filterOut(_ elements: Array, _ outs: Array) -> Elements { + let output: Elements = Elements() + for el: Element in elements { + var found: Bool = false + for out: Element in outs { + if (el.equals(out)) { + found = true + break + } + } + if (!found) { + output.add(el) + } + } + return output + } +} diff --git a/Swiftgram/SwiftSoup/Sources/DataNode.swift b/Swiftgram/SwiftSoup/Sources/DataNode.swift new file mode 100644 index 00000000000..37f7199fa12 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/DataNode.swift @@ -0,0 +1,85 @@ +// +// DataNode.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + A data node, for contents of style, script tags etc, where contents should not show in text(). + */ +open class DataNode: Node { + private static let DATA_KEY: String = "data" + + /** + Create a new DataNode. + @param data data contents + @param baseUri base URI + */ + public init(_ data: String, _ baseUri: String) { + super.init(baseUri) + do { + try attributes?.put(DataNode.DATA_KEY, data) + } catch {} + + } + + open override func nodeName() -> String { + return "#data" + } + + /** + Get the data contents of this node. Will be unescaped and with original new lines, space etc. + @return data + */ + open func getWholeData() -> String { + return attributes!.get(key: DataNode.DATA_KEY) + } + + /** + * Set the data contents of this node. + * @param data unencoded data + * @return this node, for chaining + */ + @discardableResult + open func setWholeData(_ data: String) -> DataNode { + do { + try attributes?.put(DataNode.DATA_KEY, data) + } catch {} + return self + } + + override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings)throws { + accum.append(getWholeData()) // data is not escaped in return from data nodes, so " in script, style is plain + } + + override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {} + + /** + Create a new DataNode from HTML encoded data. + @param encodedData encoded data + @param baseUri bass URI + @return new DataNode + */ + public static func createFromEncoded(_ encodedData: String, _ baseUri: String)throws->DataNode { + let data = try Entities.unescape(encodedData) + return DataNode(data, baseUri) + } + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = DataNode(attributes!.get(key: DataNode.DATA_KEY), baseUri!) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = DataNode(attributes!.get(key: DataNode.DATA_KEY), baseUri!) + return copy(clone: clone, parent: parent) + } + + public override func copy(clone: Node, parent: Node?) -> Node { + return super.copy(clone: clone, parent: parent) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/DataUtil.swift b/Swiftgram/SwiftSoup/Sources/DataUtil.swift new file mode 100644 index 00000000000..f2d0deec4e1 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/DataUtil.swift @@ -0,0 +1,24 @@ +// +// DataUtil.swift +// SwifSoup +// +// Created by Nabil Chatbi on 02/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * Internal static utilities for handling data. + * + */ +class DataUtil { + + static let charsetPattern = "(?i)\\bcharset=\\s*(?:\"|')?([^\\s,;\"']*)" + static let defaultCharset = "UTF-8" // used if not found in header or meta charset + static let bufferSize = 0x20000 // ~130K. + static let UNICODE_BOM = 0xFEFF + static let mimeBoundaryChars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + static let boundaryLength = 32 + +} diff --git a/Swiftgram/SwiftSoup/Sources/Document.swift b/Swiftgram/SwiftSoup/Sources/Document.swift new file mode 100644 index 00000000000..12e29cb514a --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Document.swift @@ -0,0 +1,562 @@ +// +// Document.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +open class Document: Element { + public enum QuirksMode { + case noQuirks, quirks, limitedQuirks + } + + private var _outputSettings: OutputSettings = OutputSettings() + private var _quirksMode: Document.QuirksMode = QuirksMode.noQuirks + private let _location: String + private var updateMetaCharset: Bool = false + + /** + Create a new, empty Document. + @param baseUri base URI of document + @see SwiftSoup#parse + @see #createShell + */ + public init(_ baseUri: String) { + self._location = baseUri + super.init(try! Tag.valueOf("#root", ParseSettings.htmlDefault), baseUri) + } + + /** + Create a valid, empty shell of a document, suitable for adding more elements to. + @param baseUri baseUri of document + @return document with html, head, and body elements. + */ + static public func createShell(_ baseUri: String) -> Document { + let doc: Document = Document(baseUri) + let html: Element = try! doc.appendElement("html") + try! html.appendElement("head") + try! html.appendElement("body") + + return doc + } + + /** + * Get the URL this Document was parsed from. If the starting URL is a redirect, + * this will return the final URL from which the document was served from. + * @return location + */ + public func location() -> String { + return _location + } + + /** + Accessor to the document's {@code head} element. + @return {@code head} + */ + public func head() -> Element? { + return findFirstElementByTagName("head", self) + } + + /** + Accessor to the document's {@code body} element. + @return {@code body} + */ + public func body() -> Element? { + return findFirstElementByTagName("body", self) + } + + /** + Get the string contents of the document's {@code title} element. + @return Trimmed title, or empty string if none set. + */ + public func title()throws->String { + // title is a preserve whitespace tag (for document output), but normalised here + let titleEl: Element? = try getElementsByTag("title").first() + return titleEl != nil ? try StringUtil.normaliseWhitespace(titleEl!.text()).trim() : "" + } + + /** + Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if + not present + @param title string to set as title + */ + public func title(_ title: String)throws { + let titleEl: Element? = try getElementsByTag("title").first() + if (titleEl == nil) { // add to head + try head()?.appendElement("title").text(title) + } else { + try titleEl?.text(title) + } + } + + /** + Create a new Element, with this document's base uri. Does not make the new element a child of this document. + @param tagName element tag name (e.g. {@code a}) + @return new element + */ + public func createElement(_ tagName: String)throws->Element { + return try Element(Tag.valueOf(tagName, ParseSettings.preserveCase), self.getBaseUri()) + } + + /** + Normalise the document. This happens after the parse phase so generally does not need to be called. + Moves any text content that is not in the body element into the body. + @return this document after normalisation + */ + @discardableResult + public func normalise()throws->Document { + var htmlE: Element? = findFirstElementByTagName("html", self) + if (htmlE == nil) { + htmlE = try appendElement("html") + } + let htmlEl: Element = htmlE! + + if (head() == nil) { + try htmlEl.prependElement("head") + } + if (body() == nil) { + try htmlEl.appendElement("body") + } + + // pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care + // of. do in inverse order to maintain text order. + try normaliseTextNodes(head()!) + try normaliseTextNodes(htmlEl) + try normaliseTextNodes(self) + + try normaliseStructure("head", htmlEl) + try normaliseStructure("body", htmlEl) + + try ensureMetaCharsetElement() + + return self + } + + // does not recurse. + private func normaliseTextNodes(_ element: Element)throws { + var toMove: Array = Array() + for node: Node in element.childNodes { + if let tn = (node as? TextNode) { + if (!tn.isBlank()) { + toMove.append(tn) + } + } + } + + for i in (0.. or contents into one, delete the remainder, and ensure they are owned by + private func normaliseStructure(_ tag: String, _ htmlEl: Element)throws { + let elements: Elements = try self.getElementsByTag(tag) + let master: Element? = elements.first() // will always be available as created above if not existent + if (elements.size() > 1) { // dupes, move contents to master + var toMove: Array = Array() + for i in 1.. + if (!(master != nil && master!.parent() != nil && master!.parent()!.equals(htmlEl))) { + try htmlEl.appendChild(master!) // includes remove() + } + } + + // fast method to get first by tag name, used for html, head, body finders + private func findFirstElementByTagName(_ tag: String, _ node: Node) -> Element? { + if (node.nodeName()==tag) { + return node as? Element + } else { + for child: Node in node.childNodes { + let found: Element? = findFirstElementByTagName(tag, child) + if (found != nil) { + return found + } + } + } + return nil + } + + open override func outerHtml()throws->String { + return try super.html() // no outer wrapper tag + } + + /** + Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared. + @param text unencoded text + @return this document + */ + @discardableResult + public override func text(_ text: String)throws->Element { + try body()?.text(text) // overridden to not nuke doc structure + return self + } + + open override func nodeName() -> String { + return "#document" + } + + /** + * Sets the charset used in this document. This method is equivalent + * to {@link OutputSettings#charset(java.nio.charset.Charset) + * OutputSettings.charset(Charset)} but in addition it updates the + * charset / encoding element within the document. + * + *

This enables + * {@link #updateMetaCharsetElement(boolean) meta charset update}.

+ * + *

If there's no element with charset / encoding information yet it will + * be created. Obsolete charset / encoding definitions are removed!

+ * + *

Elements used:

+ * + *
    + *
  • Html: <meta charset="CHARSET">
  • + *
  • Xml: <?xml version="1.0" encoding="CHARSET">
  • + *
+ * + * @param charset Charset + * + * @see #updateMetaCharsetElement(boolean) + * @see OutputSettings#charset(java.nio.charset.Charset) + */ + public func charset(_ charset: String.Encoding)throws { + updateMetaCharsetElement(true) + _outputSettings.charset(charset) + try ensureMetaCharsetElement() + } + + /** + * Returns the charset used in this document. This method is equivalent + * to {@link OutputSettings#charset()}. + * + * @return Current Charset + * + * @see OutputSettings#charset() + */ + public func charset()->String.Encoding { + return _outputSettings.charset() + } + + /** + * Sets whether the element with charset information in this document is + * updated on changes through {@link #charset(java.nio.charset.Charset) + * Document.charset(Charset)} or not. + * + *

If set to false (default) there are no elements + * modified.

+ * + * @param update If true the element updated on charset + * changes, false if not + * + * @see #charset(java.nio.charset.Charset) + */ + public func updateMetaCharsetElement(_ update: Bool) { + self.updateMetaCharset = update + } + + /** + * Returns whether the element with charset information in this document is + * updated on changes through {@link #charset(java.nio.charset.Charset) + * Document.charset(Charset)} or not. + * + * @return Returns true if the element is updated on charset + * changes, false if not + */ + public func updateMetaCharsetElement() -> Bool { + return updateMetaCharset + } + + /** + * Ensures a meta charset (html) or xml declaration (xml) with the current + * encoding used. This only applies with + * {@link #updateMetaCharsetElement(boolean) updateMetaCharset} set to + * true, otherwise this method does nothing. + * + *
    + *
  • An exsiting element gets updated with the current charset
  • + *
  • If there's no element yet it will be inserted
  • + *
  • Obsolete elements are removed
  • + *
+ * + *

Elements used:

+ * + *
    + *
  • Html: <meta charset="CHARSET">
  • + *
  • Xml: <?xml version="1.0" encoding="CHARSET">
  • + *
+ */ + private func ensureMetaCharsetElement()throws { + if (updateMetaCharset) { + let syntax: OutputSettings.Syntax = outputSettings().syntax() + + if (syntax == OutputSettings.Syntax.html) { + let metaCharset: Element? = try select("meta[charset]").first() + + if (metaCharset != nil) { + try metaCharset?.attr("charset", charset().displayName()) + } else { + let head: Element? = self.head() + + if (head != nil) { + try head?.appendElement("meta").attr("charset", charset().displayName()) + } + } + + // Remove obsolete elements + let s = try select("meta[name=charset]") + try s.remove() + + } else if (syntax == OutputSettings.Syntax.xml) { + let node: Node = getChildNodes()[0] + + if let decl = (node as? XmlDeclaration) { + + if (decl.name()=="xml") { + try decl.attr("encoding", charset().displayName()) + + _ = try decl.attr("version") + try decl.attr("version", "1.0") + } else { + try Validate.notNull(obj: baseUri) + let decl = XmlDeclaration("xml", baseUri!, false) + try decl.attr("version", "1.0") + try decl.attr("encoding", charset().displayName()) + + try prependChild(decl) + } + } else { + try Validate.notNull(obj: baseUri) + let decl = XmlDeclaration("xml", baseUri!, false) + try decl.attr("version", "1.0") + try decl.attr("encoding", charset().displayName()) + + try prependChild(decl) + } + } + } + } + + /** + * Get the document's current output settings. + * @return the document's current output settings. + */ + public func outputSettings() -> OutputSettings { + return _outputSettings + } + + /** + * Set the document's output settings. + * @param outputSettings new output settings. + * @return this document, for chaining. + */ + @discardableResult + public func outputSettings(_ outputSettings: OutputSettings) -> Document { + self._outputSettings = outputSettings + return self + } + + public func quirksMode()->Document.QuirksMode { + return _quirksMode + } + + @discardableResult + public func quirksMode(_ quirksMode: Document.QuirksMode) -> Document { + self._quirksMode = quirksMode + return self + } + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = Document(_location) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = Document(_location) + return copy(clone: clone, parent: parent) + } + + public override func copy(clone: Node, parent: Node?) -> Node { + let clone = clone as! Document + clone._outputSettings = _outputSettings.copy() as! OutputSettings + clone._quirksMode = _quirksMode + clone.updateMetaCharset = updateMetaCharset + return super.copy(clone: clone, parent: parent) + } + +} + +public class OutputSettings: NSCopying { + /** + * The output serialization syntax. + */ + public enum Syntax {case html, xml} + + private var _escapeMode: Entities.EscapeMode = Entities.EscapeMode.base + private var _encoder: String.Encoding = String.Encoding.utf8 // Charset.forName("UTF-8") + private var _prettyPrint: Bool = true + private var _outline: Bool = false + private var _indentAmount: UInt = 1 + private var _syntax = Syntax.html + + public init() {} + + /** + * Get the document's current HTML escape mode: base, which provides a limited set of named HTML + * entities and escapes other characters as numbered entities for maximum compatibility; or extended, + * which uses the complete set of HTML named entities. + *

+ * The default escape mode is base. + * @return the document's current escape mode + */ + public func escapeMode() -> Entities.EscapeMode { + return _escapeMode + } + + /** + * Set the document's escape mode, which determines how characters are escaped when the output character set + * does not support a given character:- using either a named or a numbered escape. + * @param escapeMode the new escape mode to use + * @return the document's output settings, for chaining + */ + @discardableResult + public func escapeMode(_ escapeMode: Entities.EscapeMode) -> OutputSettings { + self._escapeMode = escapeMode + return self + } + + /** + * Get the document's current output charset, which is used to control which characters are escaped when + * generating HTML (via the html() methods), and which are kept intact. + *

+ * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the + * input charset. Otherwise, it defaults to UTF-8. + * @return the document's current charset. + */ + public func encoder() -> String.Encoding { + return _encoder + } + public func charset() -> String.Encoding { + return _encoder + } + + /** + * Update the document's output charset. + * @param charset the new charset to use. + * @return the document's output settings, for chaining + */ + @discardableResult + public func encoder(_ encoder: String.Encoding) -> OutputSettings { + self._encoder = encoder + return self + } + + @discardableResult + public func charset(_ e: String.Encoding) -> OutputSettings { + return encoder(e) + } + + /** + * Get the document's current output syntax. + * @return current syntax + */ + public func syntax() -> Syntax { + return _syntax + } + + /** + * Set the document's output syntax. Either {@code html}, with empty tags and boolean attributes (etc), or + * {@code xml}, with self-closing tags. + * @param syntax serialization syntax + * @return the document's output settings, for chaining + */ + @discardableResult + public func syntax(syntax: Syntax) -> OutputSettings { + _syntax = syntax + return self + } + + /** + * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format + * the output, and the output will generally look like the input. + * @return if pretty printing is enabled. + */ + public func prettyPrint() -> Bool { + return _prettyPrint + } + + /** + * Enable or disable pretty printing. + * @param pretty new pretty print setting + * @return this, for chaining + */ + @discardableResult + public func prettyPrint(pretty: Bool) -> OutputSettings { + _prettyPrint = pretty + return self + } + + /** + * Get if outline mode is enabled. Default is false. If enabled, the HTML output methods will consider + * all tags as block. + * @return if outline mode is enabled. + */ + public func outline() -> Bool { + return _outline + } + + /** + * Enable or disable HTML outline mode. + * @param outlineMode new outline setting + * @return this, for chaining + */ + @discardableResult + public func outline(outlineMode: Bool) -> OutputSettings { + _outline = outlineMode + return self + } + + /** + * Get the current tag indent amount, used when pretty printing. + * @return the current indent amount + */ + public func indentAmount() -> UInt { + return _indentAmount + } + + /** + * Set the indent amount for pretty printing + * @param indentAmount number of spaces to use for indenting each level. Must be {@literal >=} 0. + * @return this, for chaining + */ + @discardableResult + public func indentAmount(indentAmount: UInt) -> OutputSettings { + _indentAmount = indentAmount + return self + } + + public func copy(with zone: NSZone? = nil) -> Any { + let clone: OutputSettings = OutputSettings() + clone.charset(_encoder) // new charset and charset encoder + clone._escapeMode = _escapeMode//Entities.EscapeMode.valueOf(escapeMode.name()) + // indentAmount, prettyPrint are primitives so object.clone() will handle + return clone + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/DocumentType.swift b/Swiftgram/SwiftSoup/Sources/DocumentType.swift new file mode 100644 index 00000000000..95f9b10df31 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/DocumentType.swift @@ -0,0 +1,129 @@ +// +// DocumentType.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * A {@code } node. + */ +public class DocumentType: Node { + static let PUBLIC_KEY: String = "PUBLIC" + static let SYSTEM_KEY: String = "SYSTEM" + private static let NAME: String = "name" + private static let PUB_SYS_KEY: String = "pubSysKey"; // PUBLIC or SYSTEM + private static let PUBLIC_ID: String = "publicId" + private static let SYSTEM_ID: String = "systemId" + // todo: quirk mode from publicId and systemId + + /** + * Create a new doctype element. + * @param name the doctype's name + * @param publicId the doctype's public ID + * @param systemId the doctype's system ID + * @param baseUri the doctype's base URI + */ + public init(_ name: String, _ publicId: String, _ systemId: String, _ baseUri: String) { + super.init(baseUri) + do { + try attr(DocumentType.NAME, name) + try attr(DocumentType.PUBLIC_ID, publicId) + if (has(DocumentType.PUBLIC_ID)) { + try attr(DocumentType.PUB_SYS_KEY, DocumentType.PUBLIC_KEY) + } + try attr(DocumentType.SYSTEM_ID, systemId) + } catch {} + } + + /** + * Create a new doctype element. + * @param name the doctype's name + * @param publicId the doctype's public ID + * @param systemId the doctype's system ID + * @param baseUri the doctype's base URI + */ + public init(_ name: String, _ pubSysKey: String?, _ publicId: String, _ systemId: String, _ baseUri: String) { + super.init(baseUri) + do { + try attr(DocumentType.NAME, name) + if(pubSysKey != nil) { + try attr(DocumentType.PUB_SYS_KEY, pubSysKey!) + } + try attr(DocumentType.PUBLIC_ID, publicId) + try attr(DocumentType.SYSTEM_ID, systemId) + } catch {} + } + + public override func nodeName() -> String { + return "#doctype" + } + + override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { + if (out.syntax() == OutputSettings.Syntax.html && !has(DocumentType.PUBLIC_ID) && !has(DocumentType.SYSTEM_ID)) { + // looks like a html5 doctype, go lowercase for aesthetics + accum.append("") + } + + override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { + } + + private func has(_ attribute: String) -> Bool { + do { + return !StringUtil.isBlank(try attr(attribute)) + } catch {return false} + } + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = DocumentType(attributes!.get(key: DocumentType.NAME), + attributes!.get(key: DocumentType.PUBLIC_ID), + attributes!.get(key: DocumentType.SYSTEM_ID), + baseUri!) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = DocumentType(attributes!.get(key: DocumentType.NAME), + attributes!.get(key: DocumentType.PUBLIC_ID), + attributes!.get(key: DocumentType.SYSTEM_ID), + baseUri!) + return copy(clone: clone, parent: parent) + } + + public override func copy(clone: Node, parent: Node?) -> Node { + return super.copy(clone: clone, parent: parent) + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/Element.swift b/Swiftgram/SwiftSoup/Sources/Element.swift new file mode 100644 index 00000000000..630b9914bc2 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Element.swift @@ -0,0 +1,1316 @@ +// +// Element.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +open class Element: Node { + var _tag: Tag + + private static let classString = "class" + private static let emptyString = "" + private static let idString = "id" + private static let rootString = "#root" + + //private static let classSplit : Pattern = Pattern("\\s+") + private static let classSplit = "\\s+" + + /** + * Create a new, standalone Element. (Standalone in that is has no parent.) + * + * @param tag tag of this element + * @param baseUri the base URI + * @param attributes initial attributes + * @see #appendChild(Node) + * @see #appendElement(String) + */ + public init(_ tag: Tag, _ baseUri: String, _ attributes: Attributes) { + self._tag = tag + super.init(baseUri, attributes) + } + /** + * Create a new Element from a tag and a base URI. + * + * @param tag element tag + * @param baseUri the base URI of this element. It is acceptable for the base URI to be an empty + * string, but not null. + * @see Tag#valueOf(String, ParseSettings) + */ + public init(_ tag: Tag, _ baseUri: String) { + self._tag = tag + super.init(baseUri, Attributes()) + } + + open override func nodeName() -> String { + return _tag.getName() + } + /** + * Get the name of the tag for this element. E.g. {@code div} + * + * @return the tag name + */ + open func tagName() -> String { + return _tag.getName() + } + open func tagNameNormal() -> String { + return _tag.getNameNormal() + } + + /** + * Change the tag of this element. For example, convert a {@code } to a {@code

} with + * {@code el.tagName("div")}. + * + * @param tagName new tag name for this element + * @return this element, for chaining + */ + @discardableResult + public func tagName(_ tagName: String)throws->Element { + try Validate.notEmpty(string: tagName, msg: "Tag name must not be empty.") + _tag = try Tag.valueOf(tagName, ParseSettings.preserveCase) // preserve the requested tag case + return self + } + + /** + * Get the Tag for this element. + * + * @return the tag object + */ + open func tag() -> Tag { + return _tag + } + + /** + * Test if this element is a block-level element. (E.g. {@code
== true} or an inline element + * {@code

== false}). + * + * @return true if block, false if not (and thus inline) + */ + open func isBlock() -> Bool { + return _tag.isBlock() + } + + /** + * Get the {@code id} attribute of this element. + * + * @return The id attribute, if present, or an empty string if not. + */ + open func id() -> String { + guard let attributes = attributes else {return Element.emptyString} + do { + return try attributes.getIgnoreCase(key: Element.idString) + } catch {} + return Element.emptyString + } + + /** + * Set an attribute value on this element. If this element already has an attribute with the + * key, its value is updated; otherwise, a new attribute is added. + * + * @return this element + */ + @discardableResult + open override func attr(_ attributeKey: String, _ attributeValue: String)throws->Element { + try super.attr(attributeKey, attributeValue) + return self + } + + /** + * Set a boolean attribute value on this element. Setting to true sets the attribute value to "" and + * marks the attribute as boolean so no value is written out. Setting to false removes the attribute + * with the same key if it exists. + * + * @param attributeKey the attribute key + * @param attributeValue the attribute value + * + * @return this element + */ + @discardableResult + open func attr(_ attributeKey: String, _ attributeValue: Bool)throws->Element { + try attributes?.put(attributeKey, attributeValue) + return self + } + + /** + * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key + * starting with "data-" is included the dataset. + *

+ * E.g., the element {@code

...} has the dataset + * {@code package=SwiftSoup, language=java}. + *

+ * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected + * in the other map. + *

+ * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector. + * @return a map of {@code key=value} custom data attributes. + */ + open func dataset()->Dictionary { + return attributes!.dataset() + } + + open override func parent() -> Element? { + return parentNode as? Element + } + + /** + * Get this element's parent and ancestors, up to the document root. + * @return this element's stack of parents, closest first. + */ + open func parents() -> Elements { + let parents: Elements = Elements() + Element.accumulateParents(self, parents) + return parents + } + + private static func accumulateParents(_ el: Element, _ parents: Elements) { + let parent: Element? = el.parent() + if (parent != nil && !(parent!.tagName() == Element.rootString)) { + parents.add(parent!) + accumulateParents(parent!, parents) + } + } + + /** + * Get a child element of this element, by its 0-based index number. + *

+ * Note that an element can have both mixed Nodes and Elements as children. This method inspects + * a filtered list of children that are elements, and the index is based on that filtered list. + *

+ * + * @param index the index number of the element to retrieve + * @return the child element, if it exists, otherwise throws an {@code IndexOutOfBoundsException} + * @see #childNode(int) + */ + open func child(_ index: Int) -> Element { + return children().get(index) + } + + /** + * Get this element's child elements. + *

+ * This is effectively a filter on {@link #childNodes()} to get Element nodes. + *

+ * @return child elements. If this element has no children, returns an + * empty list. + * @see #childNodes() + */ + open func children() -> Elements { + // create on the fly rather than maintaining two lists. if gets slow, memoize, and mark dirty on change + var elements = Array() + for node in childNodes { + if let n = node as? Element { + elements.append(n) + } + } + return Elements(elements) + } + + /** + * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated. + *

+ * This is effectively a filter on {@link #childNodes()} to get Text nodes. + * @return child text nodes. If this element has no text nodes, returns an + * empty list. + *

+ * For example, with the input HTML: {@code

One Two Three
Four

} with the {@code p} element selected: + *
    + *
  • {@code p.text()} = {@code "One Two Three Four"}
  • + *
  • {@code p.ownText()} = {@code "One Three Four"}
  • + *
  • {@code p.children()} = {@code Elements[,
    ]}
  • + *
  • {@code p.childNodes()} = {@code List["One ", , " Three ",
    , " Four"]}
  • + *
  • {@code p.textNodes()} = {@code List["One ", " Three ", " Four"]}
  • + *
+ */ + open func textNodes()->Array { + var textNodes = Array() + for node in childNodes { + if let n = node as? TextNode { + textNodes.append(n) + } + } + return textNodes + } + + /** + * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated. + *

+ * This is effectively a filter on {@link #childNodes()} to get Data nodes. + *

+ * @return child data nodes. If this element has no data nodes, returns an + * empty list. + * @see #data() + */ + open func dataNodes()->Array { + var dataNodes = Array() + for node in childNodes { + if let n = node as? DataNode { + dataNodes.append(n) + } + } + return dataNodes + } + + /** + * Find elements that match the {@link CssSelector} CSS query, with this element as the starting context. Matched elements + * may include this element, or any of its children. + *

+ * This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because + * multiple filters can be combined, e.g.: + *

+ *
    + *
  • {@code el.select("a[href]")} - finds links ({@code a} tags with {@code href} attributes) + *
  • {@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely) + *
+ *

+ * See the query syntax documentation in {@link CssSelector}. + *

+ * + * @param cssQuery a {@link CssSelector} CSS-like query + * @return elements that match the query (empty if none match) + * @see CssSelector + * @throws CssSelector.SelectorParseException (unchecked) on an invalid CSS query. + */ + public func select(_ cssQuery: String)throws->Elements { + return try CssSelector.select(cssQuery, self) + } + + /** + * Check if this element matches the given {@link CssSelector} CSS query. + * @param cssQuery a {@link CssSelector} CSS query + * @return if this element matches the query + */ + public func iS(_ cssQuery: String)throws->Bool { + return try iS(QueryParser.parse(cssQuery)) + } + + /** + * Check if this element matches the given {@link CssSelector} CSS query. + * @param cssQuery a {@link CssSelector} CSS query + * @return if this element matches the query + */ + public func iS(_ evaluator: Evaluator)throws->Bool { + guard let od = self.ownerDocument() else { + return false + } + return try evaluator.matches(od, self) + } + + /** + * Add a node child node to this element. + * + * @param child node to add. + * @return this element, so that you can add more child nodes or elements. + */ + @discardableResult + public func appendChild(_ child: Node)throws->Element { + // was - Node#addChildren(child). short-circuits an array create and a loop. + try reparentChild(child) + ensureChildNodes() + childNodes.append(child) + child.setSiblingIndex(childNodes.count - 1) + return self + } + + /** + * Add a node to the start of this element's children. + * + * @param child node to add. + * @return this element, so that you can add more child nodes or elements. + */ + @discardableResult + public func prependChild(_ child: Node)throws->Element { + try addChildren(0, child) + return self + } + + /** + * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the + * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first. + * + * @param index 0-based index to insert children at. Specify {@code 0} to insert at the start, {@code -1} at the + * end + * @param children child nodes to insert + * @return this element, for chaining. + */ + @discardableResult + public func insertChildren(_ index: Int, _ children: Array)throws->Element { + //Validate.notNull(children, "Children collection to be inserted must not be null.") + var index = index + let currentSize: Int = childNodeSize() + if (index < 0) { index += currentSize + 1} // roll around + try Validate.isTrue(val: index >= 0 && index <= currentSize, msg: "Insert position out of bounds.") + + try addChildren(index, children) + return self + } + + /** + * Create a new element by tag name, and add it as the last child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.appendElement("h1").attr("id", "header").text("Welcome")} + */ + @discardableResult + public func appendElement(_ tagName: String)throws->Element { + let child: Element = Element(try Tag.valueOf(tagName), getBaseUri()) + try appendChild(child) + return child + } + + /** + * Create a new element by tag name, and add it as the first child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.prependElement("h1").attr("id", "header").text("Welcome")} + */ + @discardableResult + public func prependElement(_ tagName: String)throws->Element { + let child: Element = Element(try Tag.valueOf(tagName), getBaseUri()) + try prependChild(child) + return child + } + + /** + * Create and append a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + @discardableResult + public func appendText(_ text: String)throws->Element { + let node: TextNode = TextNode(text, getBaseUri()) + try appendChild(node) + return self + } + + /** + * Create and prepend a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + @discardableResult + public func prependText(_ text: String)throws->Element { + let node: TextNode = TextNode(text, getBaseUri()) + try prependChild(node) + return self + } + + /** + * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children. + * @param html HTML to add inside this element, after the existing HTML + * @return this element + * @see #html(String) + */ + @discardableResult + public func append(_ html: String)throws->Element { + let nodes: Array = try Parser.parseFragment(html, self, getBaseUri()) + try addChildren(nodes) + return self + } + + /** + * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children. + * @param html HTML to add inside this element, before the existing HTML + * @return this element + * @see #html(String) + */ + @discardableResult + public func prepend(_ html: String)throws->Element { + let nodes: Array = try Parser.parseFragment(html, self, getBaseUri()) + try addChildren(0, nodes) + return self + } + + /** + * Insert the specified HTML into the DOM before this element (as a preceding sibling). + * + * @param html HTML to add before this element + * @return this element, for chaining + * @see #after(String) + */ + @discardableResult + open override func before(_ html: String)throws->Element { + return try super.before(html) as! Element + } + + /** + * Insert the specified node into the DOM before this node (as a preceding sibling). + * @param node to add before this element + * @return this Element, for chaining + * @see #after(Node) + */ + @discardableResult + open override func before(_ node: Node)throws->Element { + return try super.before(node) as! Element + } + + /** + * Insert the specified HTML into the DOM after this element (as a following sibling). + * + * @param html HTML to add after this element + * @return this element, for chaining + * @see #before(String) + */ + @discardableResult + open override func after(_ html: String)throws->Element { + return try super.after(html) as! Element + } + + /** + * Insert the specified node into the DOM after this node (as a following sibling). + * @param node to add after this element + * @return this element, for chaining + * @see #before(Node) + */ + open override func after(_ node: Node)throws->Element { + return try super.after(node) as! Element + } + + /** + * Remove all of the element's child nodes. Any attributes are left as-is. + * @return this element + */ + @discardableResult + public func empty() -> Element { + childNodes.removeAll() + return self + } + + /** + * Wrap the supplied HTML around this element. + * + * @param html HTML to wrap around this element, e.g. {@code
}. Can be arbitrarily deep. + * @return this element, for chaining. + */ + @discardableResult + open override func wrap(_ html: String)throws->Element { + return try super.wrap(html) as! Element + } + + /** + * Get a CSS selector that will uniquely select this element. + *

+ * If the element has an ID, returns #id; + * otherwise returns the parent (if any) CSS selector, followed by {@literal '>'}, + * followed by a unique selector for the element (tag.class.class:nth-child(n)). + *

+ * + * @return the CSS Path that can be used to retrieve the element in a selector. + */ + public func cssSelector()throws->String { + let elementId = id() + if (elementId.count > 0) { + return "#" + elementId + } + + // Translate HTML namespace ns:tag to CSS namespace syntax ns|tag + let tagName: String = self.tagName().replacingOccurrences(of: ":", with: "|") + var selector: String = tagName + let cl = try classNames() + let classes: String = cl.joined(separator: ".") + if (classes.count > 0) { + selector.append(".") + selector.append(classes) + } + + if (parent() == nil || ((parent() as? Document) != nil)) // don't add Document to selector, as will always have a html node + { + return selector + } + + selector.insert(contentsOf: " > ", at: selector.startIndex) + if (try parent()!.select(selector).array().count > 1) { + selector.append(":nth-child(\(try elementSiblingIndex() + 1))") + } + + return try parent()!.cssSelector() + (selector) + } + + /** + * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling + * of itself, so will not be included in the returned list. + * @return sibling elements + */ + public func siblingElements() -> Elements { + if (parentNode == nil) {return Elements()} + + let elements: Array? = parent()?.children().array() + let siblings: Elements = Elements() + if let elements = elements { + for el: Element in elements { + if (el != self) { + siblings.add(el) + } + } + } + return siblings + } + + /** + * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s, + * the {@code nextElementSibling} of the first {@code p} is the second {@code p}. + *

+ * This is similar to {@link #nextSibling()}, but specifically finds only Elements + *

+ * @return the next element, or null if there is no next element + * @see #previousElementSibling() + */ + public func nextElementSibling()throws->Element? { + if (parentNode == nil) {return nil} + let siblings: Array? = parent()?.children().array() + let index: Int? = try Element.indexInList(self, siblings) + try Validate.notNull(obj: index) + if let siblings = siblings { + if (siblings.count > index!+1) { + return siblings[index!+1] + } else { + return nil} + } + return nil + } + + /** + * Gets the previous element sibling of this element. + * @return the previous element, or null if there is no previous element + * @see #nextElementSibling() + */ + public func previousElementSibling()throws->Element? { + if (parentNode == nil) {return nil} + let siblings: Array? = parent()?.children().array() + let index: Int? = try Element.indexInList(self, siblings) + try Validate.notNull(obj: index) + if (index! > 0) { + return siblings?[index!-1] + } else { + return nil + } + } + + /** + * Gets the first element sibling of this element. + * @return the first sibling that is an element (aka the parent's first element child) + */ + public func firstElementSibling() -> Element? { + // todo: should firstSibling() exclude this? + let siblings: Array? = parent()?.children().array() + return (siblings != nil && siblings!.count > 1) ? siblings![0] : nil + } + + /* + * Get the list index of this element in its element sibling list. I.e. if this is the first element + * sibling, returns 0. + * @return position in element sibling list + */ + public func elementSiblingIndex()throws->Int { + if (parent() == nil) {return 0} + let x = try Element.indexInList(self, parent()?.children().array()) + return x == nil ? 0 : x! + } + + /** + * Gets the last element sibling of this element + * @return the last sibling that is an element (aka the parent's last element child) + */ + public func lastElementSibling() -> Element? { + let siblings: Array? = parent()?.children().array() + return (siblings != nil && siblings!.count > 1) ? siblings![siblings!.count - 1] : nil + } + + private static func indexInList(_ search: Element, _ elements: Array?)throws->Int? { + try Validate.notNull(obj: elements) + if let elements = elements { + for i in 0..Elements { + try Validate.notEmpty(string: tagName) + let tagName = tagName.lowercased().trim() + + return try Collector.collect(Evaluator.Tag(tagName), self) + } + + /** + * Find an element by ID, including or under this element. + *

+ * Note that this finds the first matching ID, starting with this element. If you search down from a different + * starting point, it is possible to find a different element by ID. For unique element by ID within a Document, + * use {@link Document#getElementById(String)} + * @param id The ID to search for. + * @return The first matching element by ID, starting with this element, or null if none found. + */ + public func getElementById(_ id: String)throws->Element? { + try Validate.notEmpty(string: id) + + let elements: Elements = try Collector.collect(Evaluator.Id(id), self) + if (elements.array().count > 0) { + return elements.get(0) + } else { + return nil + } + } + + /** + * Find elements that have this class, including or under this element. Case insensitive. + *

+ * Elements can have multiple classes (e.g. {@code

}. This method + * checks each class, so you can find the above with {@code el.getElementsByClass("header")}. + * + * @param className the name of the class to search for. + * @return elements with the supplied class name, empty if none + * @see #hasClass(String) + * @see #classNames() + */ + public func getElementsByClass(_ className: String)throws->Elements { + try Validate.notEmpty(string: className) + + return try Collector.collect(Evaluator.Class(className), self) + } + + /** + * Find elements that have a named attribute set. Case insensitive. + * + * @param key name of the attribute, e.g. {@code href} + * @return elements that have this attribute, empty if none + */ + public func getElementsByAttribute(_ key: String)throws->Elements { + try Validate.notEmpty(string: key) + let key = key.trim() + + return try Collector.collect(Evaluator.Attribute(key), self) + } + + /** + * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements + * that have HTML5 datasets. + * @param keyPrefix name prefix of the attribute e.g. {@code data-} + * @return elements that have attribute names that start with with the prefix, empty if none. + */ + public func getElementsByAttributeStarting(_ keyPrefix: String)throws->Elements { + try Validate.notEmpty(string: keyPrefix) + let keyPrefix = keyPrefix.trim() + + return try Collector.collect(Evaluator.AttributeStarting(keyPrefix), self) + } + + /** + * Find elements that have an attribute with the specific value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that have this attribute with this value, empty if none + */ + public func getElementsByAttributeValue(_ key: String, _ value: String)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValue(key, value), self) + } + + /** + * Find elements that either do not have this attribute, or have it with a different value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that do not have a matching attribute + */ + public func getElementsByAttributeValueNot(_ key: String, _ value: String)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValueNot(key, value), self) + } + + /** + * Find elements that have attributes that start with the value prefix. Case insensitive. + * + * @param key name of the attribute + * @param valuePrefix start of attribute value + * @return elements that have attributes that start with the value prefix + */ + public func getElementsByAttributeValueStarting(_ key: String, _ valuePrefix: String)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValueStarting(key, valuePrefix), self) + } + + /** + * Find elements that have attributes that end with the value suffix. Case insensitive. + * + * @param key name of the attribute + * @param valueSuffix end of the attribute value + * @return elements that have attributes that end with the value suffix + */ + public func getElementsByAttributeValueEnding(_ key: String, _ valueSuffix: String)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValueEnding(key, valueSuffix), self) + } + + /** + * Find elements that have attributes whose value contains the match string. Case insensitive. + * + * @param key name of the attribute + * @param match substring of value to search for + * @return elements that have attributes containing this text + */ + public func getElementsByAttributeValueContaining(_ key: String, _ match: String)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValueContaining(key, match), self) + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param pattern compiled regular expression to match against attribute values + * @return elements that have attributes matching this regular expression + */ + public func getElementsByAttributeValueMatching(_ key: String, _ pattern: Pattern)throws->Elements { + return try Collector.collect(Evaluator.AttributeWithValueMatching(key, pattern), self) + + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param regex regular expression to match against attribute values. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements that have attributes matching this regular expression + */ + public func getElementsByAttributeValueMatching(_ key: String, _ regex: String)throws->Elements { + var pattern: Pattern + do { + pattern = Pattern.compile(regex) + try pattern.validate() + } catch { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Pattern syntax error: \(regex)") + } + return try getElementsByAttributeValueMatching(key, pattern) + } + + /** + * Find elements whose sibling index is less than the supplied index. + * @param index 0-based index + * @return elements less than index + */ + public func getElementsByIndexLessThan(_ index: Int)throws->Elements { + return try Collector.collect(Evaluator.IndexLessThan(index), self) + } + + /** + * Find elements whose sibling index is greater than the supplied index. + * @param index 0-based index + * @return elements greater than index + */ + public func getElementsByIndexGreaterThan(_ index: Int)throws->Elements { + return try Collector.collect(Evaluator.IndexGreaterThan(index), self) + } + + /** + * Find elements whose sibling index is equal to the supplied index. + * @param index 0-based index + * @return elements equal to index + */ + public func getElementsByIndexEquals(_ index: Int)throws->Elements { + return try Collector.collect(Evaluator.IndexEquals(index), self) + } + + /** + * Find elements that contain the specified string. The search is case insensitive. The text may appear directly + * in the element, or in any of its descendants. + * @param searchText to look for in the element's text + * @return elements that contain the string, case insensitive. + * @see Element#text() + */ + public func getElementsContainingText(_ searchText: String)throws->Elements { + return try Collector.collect(Evaluator.ContainsText(searchText), self) + } + + /** + * Find elements that directly contain the specified string. The search is case insensitive. The text must appear directly + * in the element, not in any of its descendants. + * @param searchText to look for in the element's own text + * @return elements that contain the string, case insensitive. + * @see Element#ownText() + */ + public func getElementsContainingOwnText(_ searchText: String)throws->Elements { + return try Collector.collect(Evaluator.ContainsOwnText(searchText), self) + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public func getElementsMatchingText(_ pattern: Pattern)throws->Elements { + return try Collector.collect(Evaluator.Matches(pattern), self) + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public func getElementsMatchingText(_ regex: String)throws->Elements { + let pattern: Pattern + do { + pattern = Pattern.compile(regex) + try pattern.validate() + } catch { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Pattern syntax error: \(regex)") + } + return try getElementsMatchingText(pattern) + } + + /** + * Find elements whose own text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public func getElementsMatchingOwnText(_ pattern: Pattern)throws->Elements { + return try Collector.collect(Evaluator.MatchesOwn(pattern), self) + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use embedded flags (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public func getElementsMatchingOwnText(_ regex: String)throws->Elements { + let pattern: Pattern + do { + pattern = Pattern.compile(regex) + try pattern.validate() + } catch { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Pattern syntax error: \(regex)") + } + return try getElementsMatchingOwnText(pattern) + } + + /** + * Find all elements under this element (including self, and children of children). + * + * @return all elements + */ + public func getAllElements()throws->Elements { + return try Collector.collect(Evaluator.AllElements(), self) + } + + /** + * Gets the combined text of this element and all its children. Whitespace is normalized and trimmed. + *

+ * For example, given HTML {@code

Hello there now!

}, {@code p.text()} returns {@code "Hello there now!"} + * + * @return unencoded text, or empty string if none. + * @see #ownText() + * @see #textNodes() + */ + class textNodeVisitor: NodeVisitor { + let accum: StringBuilder + let trimAndNormaliseWhitespace: Bool + init(_ accum: StringBuilder, trimAndNormaliseWhitespace: Bool) { + self.accum = accum + self.trimAndNormaliseWhitespace = trimAndNormaliseWhitespace + } + public func head(_ node: Node, _ depth: Int) { + if let textNode = (node as? TextNode) { + if trimAndNormaliseWhitespace { + Element.appendNormalisedText(accum, textNode) + } else { + accum.append(textNode.getWholeText()) + } + } else if let element = (node as? Element) { + if !accum.isEmpty && + (element.isBlock() || element._tag.getName() == "br") && + !TextNode.lastCharIsWhitespace(accum) { + accum.append(" ") + } + } + } + + public func tail(_ node: Node, _ depth: Int) { + } + } + public func text(trimAndNormaliseWhitespace: Bool = true)throws->String { + let accum: StringBuilder = StringBuilder() + try NodeTraversor(textNodeVisitor(accum, trimAndNormaliseWhitespace: trimAndNormaliseWhitespace)).traverse(self) + let text = accum.toString() + if trimAndNormaliseWhitespace { + return text.trim() + } + return text + } + + /** + * Gets the text owned by this element only; does not get the combined text of all children. + *

+ * For example, given HTML {@code

Hello there now!

}, {@code p.ownText()} returns {@code "Hello now!"}, + * whereas {@code p.text()} returns {@code "Hello there now!"}. + * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element. + * + * @return unencoded text, or empty string if none. + * @see #text() + * @see #textNodes() + */ + public func ownText() -> String { + let sb: StringBuilder = StringBuilder() + ownText(sb) + return sb.toString().trim() + } + + private func ownText(_ accum: StringBuilder) { + for child: Node in childNodes { + if let textNode = (child as? TextNode) { + Element.appendNormalisedText(accum, textNode) + } else if let child = (child as? Element) { + Element.appendWhitespaceIfBr(child, accum) + } + } + } + + private static func appendNormalisedText(_ accum: StringBuilder, _ textNode: TextNode) { + let text: String = textNode.getWholeText() + + if (Element.preserveWhitespace(textNode.parentNode)) { + accum.append(text) + } else { + StringUtil.appendNormalisedWhitespace(accum, string: text, stripLeading: TextNode.lastCharIsWhitespace(accum)) + } + } + + private static func appendWhitespaceIfBr(_ element: Element, _ accum: StringBuilder) { + if (element._tag.getName() == "br" && !TextNode.lastCharIsWhitespace(accum)) { + accum.append(" ") + } + } + + static func preserveWhitespace(_ node: Node?) -> Bool { + // looks only at this element and one level up, to prevent recursion & needless stack searches + if let element = (node as? Element) { + return element._tag.preserveWhitespace() || element.parent() != nil && element.parent()!._tag.preserveWhitespace() + } + return false + } + + /** + * Set the text of this element. Any existing contents (text or elements) will be cleared + * @param text unencoded text + * @return this element + */ + @discardableResult + public func text(_ text: String)throws->Element { + empty() + let textNode: TextNode = TextNode(text, baseUri) + try appendChild(textNode) + return self + } + + /** + Test if this element has any text content (that is not just whitespace). + @return true if element has non-blank text content. + */ + public func hasText() -> Bool { + for child: Node in childNodes { + if let textNode = (child as? TextNode) { + if (!textNode.isBlank()) { + return true + } + } else if let el = (child as? Element) { + if (el.hasText()) { + return true + } + } + } + return false + } + + /** + * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag. + * @return the data, or empty string if none + * + * @see #dataNodes() + */ + public func data() -> String { + let sb: StringBuilder = StringBuilder() + + for childNode: Node in childNodes { + if let data = (childNode as? DataNode) { + sb.append(data.getWholeData()) + } else if let element = (childNode as? Element) { + let elementData: String = element.data() + sb.append(elementData) + } + } + return sb.toString() + } + + /** + * Gets the literal value of this element's "class" attribute, which may include multiple class names, space + * separated. (E.g. on <div class="header gray"> returns, "header gray") + * @return The literal class attribute, or empty string if no class attribute set. + */ + public func className()throws->String { + return try attr(Element.classString).trim() + } + + /** + * Get all of the element's class names. E.g. on element {@code
}, + * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to + * the backing {@code class} attribute; use the {@link #classNames(java.util.Set)} method to persist them. + * @return set of classnames, empty if no class attribute + */ + public func classNames()throws->OrderedSet { + let fitted = try className().replaceAll(of: Element.classSplit, with: " ", options: .caseInsensitive) + let names: [String] = fitted.components(separatedBy: " ") + let classNames: OrderedSet = OrderedSet(sequence: names) + classNames.remove(Element.emptyString) // if classNames() was empty, would include an empty class + return classNames + } + + /** + Set the element's {@code class} attribute to the supplied class names. + @param classNames set of classes + @return this element, for chaining + */ + @discardableResult + public func classNames(_ classNames: OrderedSet)throws->Element { + try attributes?.put(Element.classString, StringUtil.join(classNames, sep: " ")) + return self + } + + /** + * Tests if this element has a class. Case insensitive. + * @param className name of class to check for + * @return true if it does, false if not + */ + // performance sensitive + public func hasClass(_ className: String) -> Bool { + let classAtt: String? = attributes?.get(key: Element.classString) + let len: Int = (classAtt != nil) ? classAtt!.count : 0 + let wantLen: Int = className.count + + if (len == 0 || len < wantLen) { + return false + } + let classAttr = classAtt! + + // if both lengths are equal, only need compare the className with the attribute + if (len == wantLen) { + return className.equalsIgnoreCase(string: classAttr) + } + + // otherwise, scan for whitespace and compare regions (with no string or arraylist allocations) + var inClass: Bool = false + var start: Int = 0 + for i in 0..Element { + let classes: OrderedSet = try classNames() + classes.append(className) + try classNames(classes) + return self + } + + /** + Remove a class name from this element's {@code class} attribute. + @param className class name to remove + @return this element + */ + @discardableResult + public func removeClass(_ className: String)throws->Element { + let classes: OrderedSet = try classNames() + classes.remove(className) + try classNames(classes) + return self + } + + /** + Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it. + @param className class name to toggle + @return this element + */ + @discardableResult + public func toggleClass(_ className: String)throws->Element { + let classes: OrderedSet = try classNames() + if (classes.contains(className)) {classes.remove(className) + } else { + classes.append(className) + } + try classNames(classes) + + return self + } + + /** + * Get the value of a form element (input, textarea, etc). + * @return the value of the form element, or empty string if not set. + */ + public func val()throws->String { + if (tagName()=="textarea") { + return try text() + } else { + return try attr("value") + } + } + + /** + * Set the value of a form element (input, textarea, etc). + * @param value value to set + * @return this element (for chaining) + */ + @discardableResult + public func val(_ value: String)throws->Element { + if (tagName() == "textarea") { + try text(value) + } else { + try attr("value", value) + } + return self + } + + override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings)throws { + if (out.prettyPrint() && (_tag.formatAsBlock() || (parent() != nil && parent()!.tag().formatAsBlock()) || out.outline())) { + if !accum.isEmpty { + indent(accum, depth, out) + } + } + accum + .append("<") + .append(tagName()) + try attributes?.html(accum: accum, out: out) + + // selfclosing includes unknown tags, isEmpty defines tags that are always empty + if (childNodes.isEmpty && _tag.isSelfClosing()) { + if (out.syntax() == OutputSettings.Syntax.html && _tag.isEmpty()) { + accum.append(">") + } else { + accum.append(" />") // in html, in xml + } + } else { + accum.append(">") + } + } + + override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { + if (!(childNodes.isEmpty && _tag.isSelfClosing())) { + if (out.prettyPrint() && (!childNodes.isEmpty && ( + _tag.formatAsBlock() || (out.outline() && (childNodes.count>1 || (childNodes.count==1 && !(((childNodes[0] as? TextNode) != nil))))) + ))) { + indent(accum, depth, out) + } + accum.append("") + } + } + + /** + * Retrieves the element's inner HTML. E.g. on a {@code
} with one empty {@code

}, would return + * {@code

}. (Whereas {@link #outerHtml()} would return {@code

}.) + * + * @return String of HTML. + * @see #outerHtml() + */ + public func html()throws->String { + let accum: StringBuilder = StringBuilder() + try html2(accum) + return getOutputSettings().prettyPrint() ? accum.toString().trim() : accum.toString() + } + + private func html2(_ accum: StringBuilder)throws { + for node in childNodes { + try node.outerHtml(accum) + } + } + + /** + * {@inheritDoc} + */ + open override func html(_ appendable: StringBuilder)throws->StringBuilder { + for node in childNodes { + try node.outerHtml(appendable) + } + return appendable + } + + /** + * Set this element's inner HTML. Clears the existing HTML first. + * @param html HTML to parse and set into this element + * @return this element + * @see #append(String) + */ + @discardableResult + public func html(_ html: String)throws->Element { + empty() + try append(html) + return self + } + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = Element(_tag, baseUri!, attributes!) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = Element(_tag, baseUri!, attributes!) + return copy(clone: clone, parent: parent) + } + public override func copy(clone: Node, parent: Node?) -> Node { + return super.copy(clone: clone, parent: parent) + } + + public static func ==(lhs: Element, rhs: Element) -> Bool { + guard lhs as Node == rhs as Node else { + return false + } + + return lhs._tag == rhs._tag + } + + override public func hash(into hasher: inout Hasher) { + super.hash(into: &hasher) + hasher.combine(_tag) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Elements.swift b/Swiftgram/SwiftSoup/Sources/Elements.swift new file mode 100644 index 00000000000..b8e3852f12d --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Elements.swift @@ -0,0 +1,657 @@ +// +// Elements.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 20/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// +/** +A list of {@link Element}s, with methods that act on every element in the list. +

+To get an {@code Elements} object, use the {@link Element#select(String)} method. +

+*/ + +import Foundation + +//open typealias Elements = Array +//typealias E = Element +open class Elements: NSCopying { + fileprivate var this: Array = Array() + + ///base init + public init() { + } + ///Initialized with an array + public init(_ a: Array) { + this = a + } + ///Initialized with an order set + public init(_ a: OrderedSet) { + this.append(contentsOf: a) + } + + /** + * Creates a deep copy of these elements. + * @return a deep copy + */ + public func copy(with zone: NSZone? = nil) -> Any { + let clone: Elements = Elements() + for e: Element in this { + clone.add(e.copy() as! Element) + } + return clone + } + + // attribute methods + /** + Get an attribute value from the first matched element that has the attribute. + @param attributeKey The attribute key. + @return The attribute value from the first matched element that has the attribute.. If no elements were matched (isEmpty() == true), + or if the no elements have the attribute, returns empty string. + @see #hasAttr(String) + */ + open func attr(_ attributeKey: String)throws->String { + for element in this { + if (element.hasAttr(attributeKey)) { + return try element.attr(attributeKey) + } + } + return "" + } + + /** + Checks if any of the matched elements have this attribute set. + @param attributeKey attribute key + @return true if any of the elements have the attribute; false if none do. + */ + open func hasAttr(_ attributeKey: String) -> Bool { + for element in this { + if element.hasAttr(attributeKey) {return true} + } + return false + } + + /** + * Set an attribute on all matched elements. + * @param attributeKey attribute key + * @param attributeValue attribute value + * @return this + */ + @discardableResult + open func attr(_ attributeKey: String, _ attributeValue: String)throws->Elements { + for element in this { + try element.attr(attributeKey, attributeValue) + } + return self + } + + /** + * Remove an attribute from every matched element. + * @param attributeKey The attribute to remove. + * @return this (for chaining) + */ + @discardableResult + open func removeAttr(_ attributeKey: String)throws->Elements { + for element in this { + try element.removeAttr(attributeKey) + } + return self + } + + /** + Add the class name to every matched element's {@code class} attribute. + @param className class name to add + @return this + */ + @discardableResult + open func addClass(_ className: String)throws->Elements { + for element in this { + try element.addClass(className) + } + return self + } + + /** + Remove the class name from every matched element's {@code class} attribute, if present. + @param className class name to remove + @return this + */ + @discardableResult + open func removeClass(_ className: String)throws->Elements { + for element: Element in this { + try element.removeClass(className) + } + return self + } + + /** + Toggle the class name on every matched element's {@code class} attribute. + @param className class name to add if missing, or remove if present, from every element. + @return this + */ + @discardableResult + open func toggleClass(_ className: String)throws->Elements { + for element: Element in this { + try element.toggleClass(className) + } + return self + } + + /** + Determine if any of the matched elements have this class name set in their {@code class} attribute. + @param className class name to check for + @return true if any do, false if none do + */ + + open func hasClass(_ className: String) -> Bool { + for element: Element in this { + if (element.hasClass(className)) { + return true + } + } + return false + } + + /** + * Get the form element's value of the first matched element. + * @return The form element's value, or empty if not set. + * @see Element#val() + */ + open func val()throws->String { + if (size() > 0) { + return try first()!.val() + } + return "" + } + + /** + * Set the form element's value in each of the matched elements. + * @param value The value to set into each matched element + * @return this (for chaining) + */ + @discardableResult + open func val(_ value: String)throws->Elements { + for element: Element in this { + try element.val(value) + } + return self + } + + /** + * Get the combined text of all the matched elements. + *

+ * Note that it is possible to get repeats if the matched elements contain both parent elements and their own + * children, as the Element.text() method returns the combined text of a parent and all its children. + * @return string of all text: unescaped and no HTML. + * @see Element#text() + */ + open func text(trimAndNormaliseWhitespace: Bool = true)throws->String { + let sb: StringBuilder = StringBuilder() + for element: Element in this { + if !sb.isEmpty { + sb.append(" ") + } + sb.append(try element.text(trimAndNormaliseWhitespace: trimAndNormaliseWhitespace)) + } + return sb.toString() + } + + /// Check if an element has text + open func hasText() -> Bool { + for element: Element in this { + if (element.hasText()) { + return true + } + } + return false + } + + /** + * Get the text content of each of the matched elements. If an element has no text, then it is not included in the + * result. + * @return A list of each matched element's text content. + * @see Element#text() + * @see Element#hasText() + * @see #text() + */ + public func eachText()throws->Array { + var texts: Array = Array() + for el: Element in this { + if (el.hasText()){ + texts.append(try el.text()) + } + } + return texts; + } + + /** + * Get the combined inner HTML of all matched elements. + * @return string of all element's inner HTML. + * @see #text() + * @see #outerHtml() + */ + open func html()throws->String { + let sb: StringBuilder = StringBuilder() + for element: Element in this { + if !sb.isEmpty { + sb.append("\n") + } + sb.append(try element.html()) + } + return sb.toString() + } + + /** + * Get the combined outer HTML of all matched elements. + * @return string of all element's outer HTML. + * @see #text() + * @see #html() + */ + open func outerHtml()throws->String { + let sb: StringBuilder = StringBuilder() + for element in this { + if !sb.isEmpty { + sb.append("\n") + } + sb.append(try element.outerHtml()) + } + return sb.toString() + } + + /** + * Get the combined outer HTML of all matched elements. Alias of {@link #outerHtml()}. + * @return string of all element's outer HTML. + * @see #text() + * @see #html() + */ + + open func toString()throws->String { + return try outerHtml() + } + + /** + * Update the tag name of each matched element. For example, to change each {@code } to a {@code }, do + * {@code doc.select("i").tagName("em");} + * @param tagName the new tag name + * @return this, for chaining + * @see Element#tagName(String) + */ + @discardableResult + open func tagName(_ tagName: String)throws->Elements { + for element: Element in this { + try element.tagName(tagName) + } + return self + } + + /** + * Set the inner HTML of each matched element. + * @param html HTML to parse and set into each matched element. + * @return this, for chaining + * @see Element#html(String) + */ + @discardableResult + open func html(_ html: String)throws->Elements { + for element: Element in this { + try element.html(html) + } + return self + } + + /** + * Add the supplied HTML to the start of each matched element's inner HTML. + * @param html HTML to add inside each element, before the existing HTML + * @return this, for chaining + * @see Element#prepend(String) + */ + @discardableResult + open func prepend(_ html: String)throws->Elements { + for element: Element in this { + try element.prepend(html) + } + return self + } + + /** + * Add the supplied HTML to the end of each matched element's inner HTML. + * @param html HTML to add inside each element, after the existing HTML + * @return this, for chaining + * @see Element#append(String) + */ + @discardableResult + open func append(_ html: String)throws->Elements { + for element: Element in this { + try element.append(html) + } + return self + } + + /** + * Insert the supplied HTML before each matched element's outer HTML. + * @param html HTML to insert before each element + * @return this, for chaining + * @see Element#before(String) + */ + @discardableResult + open func before(_ html: String)throws->Elements { + for element: Element in this { + try element.before(html) + } + return self + } + + /** + * Insert the supplied HTML after each matched element's outer HTML. + * @param html HTML to insert after each element + * @return this, for chaining + * @see Element#after(String) + */ + @discardableResult + open func after(_ html: String)throws->Elements { + for element: Element in this { + try element.after(html) + } + return self + } + + /** + Wrap the supplied HTML around each matched elements. For example, with HTML + {@code

This is SwiftSoup

}, + doc.select("b").wrap("<i></i>"); + becomes {@code

This is SwiftSoup

} + @param html HTML to wrap around each element, e.g. {@code
}. Can be arbitrarily deep. + @return this (for chaining) + @see Element#wrap + */ + @discardableResult + open func wrap(_ html: String)throws->Elements { + try Validate.notEmpty(string: html) + for element: Element in this { + try element.wrap(html) + } + return self + } + + /** + * Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of + * dropping the elements but keeping their children. + *

+ * This is useful for e.g removing unwanted formatting elements but keeping their contents. + *

+ * + * E.g. with HTML:

{@code

One Two
}

+ *

{@code doc.select("font").unwrap();}

+ *

HTML = {@code

One Two
}

+ * + * @return this (for chaining) + * @see Node#unwrap + */ + @discardableResult + open func unwrap()throws->Elements { + for element: Element in this { + try element.unwrap() + } + return self + } + + /** + * Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each + * element to nothing. + *

+ * E.g. HTML: {@code

Hello there

now

}
+ * doc.select("p").empty();
+ * HTML = {@code

} + * @return this, for chaining + * @see Element#empty() + * @see #remove() + */ + @discardableResult + open func empty() -> Elements { + for element: Element in this { + element.empty() + } + return self + } + + /** + * Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing. + *

+ * E.g. HTML: {@code

Hello

there

}
+ * doc.select("p").remove();
+ * HTML = {@code
} + *

+ * Note that this method should not be used to clean user-submitted HTML; rather, use {@link Cleaner} to clean HTML. + * @return this, for chaining + * @see Element#empty() + * @see #empty() + */ + @discardableResult + open func remove()throws->Elements { + for element in this { + try element.remove() + } + return self + } + + // filters + + /** + * Find matching elements within this element list. + * @param query A {@link CssSelector} query + * @return the filtered list of elements, or an empty list if none match. + */ + open func select(_ query: String)throws->Elements { + return try CssSelector.select(query, this) + } + + /** + * Remove elements from this list that match the {@link CssSelector} query. + *

+ * E.g. HTML: {@code

Two
}
+ * Elements divs = doc.select("div").not(".logo");
+ * Result: {@code divs: [
Two
]} + *

+ * @param query the selector query whose results should be removed from these elements + * @return a new elements list that contains only the filtered results + */ + open func not(_ query: String)throws->Elements { + let out: Elements = try CssSelector.select(query, this) + return CssSelector.filterOut(this, out.this) + } + + /** + * Get the nth matched element as an Elements object. + *

+ * See also {@link #get(int)} to retrieve an Element. + * @param index the (zero-based) index of the element in the list to retain + * @return Elements containing only the specified element, or, if that element did not exist, an empty list. + */ + open func eq(_ index: Int) -> Elements { + return size() > index ? Elements([get(index)]) : Elements() + } + + /** + * Test if any of the matched elements match the supplied query. + * @param query A selector + * @return true if at least one element in the list matches the query. + */ + open func iS(_ query: String)throws->Bool { + let eval: Evaluator = try QueryParser.parse(query) + for e: Element in this { + if (try e.iS(eval)) { + return true + } + } + return false + + } + + /** + * Get all of the parents and ancestor elements of the matched elements. + * @return all of the parents and ancestor elements of the matched elements + */ + + open func parents() -> Elements { + let combo: OrderedSet = OrderedSet() + for e: Element in this { + combo.append(contentsOf: e.parents().array()) + } + return Elements(combo) + } + + // list-like methods + /** + Get the first matched element. + @return The first matched element, or null if contents is empty. + */ + open func first() -> Element? { + return isEmpty() ? nil : get(0) + } + + /// Check if no element stored + open func isEmpty() -> Bool { + return array().count == 0 + } + + /// Count + open func size() -> Int { + return array().count + } + + /** + Get the last matched element. + @return The last matched element, or null if contents is empty. + */ + open func last() -> Element? { + return isEmpty() ? nil : get(size() - 1) + } + + /** + * Perform a depth-first traversal on each of the selected elements. + * @param nodeVisitor the visitor callbacks to perform on each node + * @return this, for chaining + */ + @discardableResult + open func traverse(_ nodeVisitor: NodeVisitor)throws->Elements { + let traversor: NodeTraversor = NodeTraversor(nodeVisitor) + for el: Element in this { + try traversor.traverse(el) + } + return self + } + + /** + * Get the {@link FormElement} forms from the selected elements, if any. + * @return a list of {@link FormElement}s pulled from the matched elements. The list will be empty if the elements contain + * no forms. + */ + open func forms()->Array { + var forms: Array = Array() + for el: Element in this { + if let el = el as? FormElement { + forms.append(el) + } + } + return forms + } + + /** + * Appends the specified element to the end of this list. + * + * @param e element to be appended to this list + * @return true (as specified by {@link Collection#add}) + */ + open func add(_ e: Element) { + this.append(e) + } + + /** + * Insert the specified element at index. + */ + open func add(_ index: Int, _ element: Element) { + this.insert(element, at: index) + } + + /// Return element at index + open func get(_ i: Int) -> Element { + return this[i] + } + + /// Returns all elements + open func array()->Array { + return this + } +} + +/** +* Elements extension Equatable. +*/ +extension Elements: Equatable { + /// Returns a Boolean value indicating whether two values are equal. + /// + /// Equality is the inverse of inequality. For any values `a` and `b`, + /// `a == b` implies that `a != b` is `false`. + /// + /// - Parameters: + /// - lhs: A value to compare. + /// - rhs: Another value to compare. + public static func ==(lhs: Elements, rhs: Elements) -> Bool { + return lhs.this == rhs.this + } +} + +/** +* Elements RandomAccessCollection +*/ +extension Elements: RandomAccessCollection { + public subscript(position: Int) -> Element { + return this[position] + } + + public var startIndex: Int { + return this.startIndex + } + + public var endIndex: Int { + return this.endIndex + } + + /// The number of Element objects in the collection. + /// Equivalent to `size()` + public var count: Int { + return this.count + } +} + +/** +* Elements IteratorProtocol. +*/ +public struct ElementsIterator: IteratorProtocol { + /// Elements reference + let elements: Elements + //current element index + var index = 0 + + /// Initializer + init(_ countdown: Elements) { + self.elements = countdown + } + + /// Advances to the next element and returns it, or `nil` if no next element + mutating public func next() -> Element? { + let result = index < elements.size() ? elements.get(index) : nil + index += 1 + return result + } +} + +/** +* Elements Extension Sequence. +*/ +extension Elements: Sequence { + /// Returns an iterator over the elements of this sequence. + public func makeIterator() -> ElementsIterator { + return ElementsIterator(self) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Entities.swift b/Swiftgram/SwiftSoup/Sources/Entities.swift new file mode 100644 index 00000000000..b513301c27e --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Entities.swift @@ -0,0 +1,338 @@ +// +// Entities.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * HTML entities, and escape routines. + * Source: W3C HTML + * named character references. + */ +public class Entities { + private static let empty = -1 + private static let emptyName = "" + private static let codepointRadix: Int = 36 + + public class EscapeMode: Equatable { + + /** Restricted entities suitable for XHTML output: lt, gt, amp, and quot only. */ + public static let xhtml: EscapeMode = EscapeMode(string: Entities.xhtml, size: 4, id: 0) + /** Default HTML output entities. */ + public static let base: EscapeMode = EscapeMode(string: Entities.base, size: 106, id: 1) + /** Complete HTML entities. */ + public static let extended: EscapeMode = EscapeMode(string: Entities.full, size: 2125, id: 2) + + fileprivate let value: Int + + struct NamedCodepoint { + let scalar: UnicodeScalar + let name: String + } + + // Array of named references, sorted by name for binary search. built by BuildEntities. + // The few entities that map to a multi-codepoint sequence go into multipoints. + fileprivate var entitiesByName: [NamedCodepoint] = [] + + // Array of entities in first-codepoint order. We don't currently support + // multicodepoints to single named value currently. Lazy because this index + // is used only when generating HTML text. + fileprivate lazy var entitiesByCodepoint = entitiesByName.sorted() { a, b in a.scalar < b.scalar } + + public static func == (left: EscapeMode, right: EscapeMode) -> Bool { + return left.value == right.value + } + + static func != (left: EscapeMode, right: EscapeMode) -> Bool { + return left.value != right.value + } + + private static let codeDelims: [UnicodeScalar] = [",", ";"] + + init(string: String, size: Int, id: Int) { + + value = id + let reader: CharacterReader = CharacterReader(string) + + entitiesByName.reserveCapacity(size) + while !reader.isEmpty() { + let name: String = reader.consumeTo("=") + reader.advance() + let cp1: Int = Int(reader.consumeToAny(EscapeMode.codeDelims), radix: codepointRadix) ?? 0 + let codeDelim: UnicodeScalar = reader.current() + reader.advance() + let cp2: Int + if (codeDelim == ",") { + cp2 = Int(reader.consumeTo(";"), radix: codepointRadix) ?? 0 + reader.advance() + } else { + cp2 = empty + } + let _ = Int(reader.consumeTo("\n"), radix: codepointRadix) ?? 0 + reader.advance() + + entitiesByName.append(NamedCodepoint(scalar: UnicodeScalar(cp1)!, name: name)) + + if (cp2 != empty) { + multipointsLock.lock() + multipoints[name] = [UnicodeScalar(cp1)!, UnicodeScalar(cp2)!] + multipointsLock.unlock() + } + } + // Entities should start in name order, but better safe than sorry... + entitiesByName.sort() { a, b in a.name < b.name } + } + + // Only returns the first of potentially multiple codepoints + public func codepointForName(_ name: String) -> UnicodeScalar? { + let ix = entitiesByName.binarySearch { $0.name < name } + guard ix < entitiesByName.endIndex else { return nil } + let entity = entitiesByName[ix] + guard entity.name == name else { return nil } + return entity.scalar + } + + // Search by first codepoint only + public func nameForCodepoint(_ codepoint: UnicodeScalar ) -> String? { + var ix = entitiesByCodepoint.binarySearch { $0.scalar < codepoint } + var matches: [String] = [] + while ix < entitiesByCodepoint.endIndex && entitiesByCodepoint[ix].scalar == codepoint { + matches.append(entitiesByCodepoint[ix].name) + ix = entitiesByCodepoint.index(after: ix) + } + return matches.isEmpty ? nil : matches.sorted().last! + } + + private func size() -> Int { + return entitiesByName.count + } + + } + + private static var multipoints: [String: [UnicodeScalar]] = [:] // name -> multiple character references + private static var multipointsLock = MutexLock() + + /** + * Check if the input is a known named entity + * @param name the possible entity name (e.g. "lt" or "amp") + * @return true if a known named entity + */ + public static func isNamedEntity(_ name: String ) -> Bool { + return (EscapeMode.extended.codepointForName(name) != nil) + } + + /** + * Check if the input is a known named entity in the base entity set. + * @param name the possible entity name (e.g. "lt" or "amp") + * @return true if a known named entity in the base set + * @see #isNamedEntity(String) + */ + public static func isBaseNamedEntity(_ name: String) -> Bool { + return EscapeMode.base.codepointForName(name) != nil + } + + /** + * Get the character(s) represented by the named entitiy + * @param name entity (e.g. "lt" or "amp") + * @return the string value of the character(s) represented by this entity, or "" if not defined + */ + public static func getByName(name: String) -> String? { + if let scalars = codepointsForName(name) { + return String(String.UnicodeScalarView(scalars)) + } + return nil + } + + public static func codepointsForName(_ name: String) -> [UnicodeScalar]? { + multipointsLock.lock() + if let scalars = multipoints[name] { + multipointsLock.unlock() + return scalars + } + multipointsLock.unlock() + + if let scalar = EscapeMode.extended.codepointForName(name) { + return [scalar] + } + return nil + } + + public static func escape(_ string: String, _ encode: String.Encoding = .utf8 ) -> String { + return Entities.escape(string, OutputSettings().charset(encode).escapeMode(Entities.EscapeMode.extended)) + } + + public static func escape(_ string: String, _ out: OutputSettings) -> String { + let accum = StringBuilder()//string.characters.count * 2 + escape(accum, string, out, false, false, false) + // try { + // + // } catch (IOException e) { + // throw new SerializationException(e) // doesn't happen + // } + return accum.toString() + } + + // this method is ugly, and does a lot. but other breakups cause rescanning and stringbuilder generations + static func escape(_ accum: StringBuilder, _ string: String, _ out: OutputSettings, _ inAttribute: Bool, _ normaliseWhite: Bool, _ stripLeadingWhite: Bool ) { + var lastWasWhite = false + var reachedNonWhite = false + let escapeMode: EscapeMode = out.escapeMode() + let encoder: String.Encoding = out.encoder() + //let length = UInt32(string.characters.count) + + var codePoint: UnicodeScalar + for ch in string.unicodeScalars { + codePoint = ch + + if (normaliseWhite) { + if (codePoint.isWhitespace) { + if ((stripLeadingWhite && !reachedNonWhite) || lastWasWhite) { + continue + } + accum.append(UnicodeScalar.Space) + lastWasWhite = true + continue + } else { + lastWasWhite = false + reachedNonWhite = true + } + } + + // surrogate pairs, split implementation for efficiency on single char common case (saves creating strings, char[]): + if (codePoint.value < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + let c = codePoint + // html specific and required escapes: + switch (codePoint) { + case UnicodeScalar.Ampersand: + accum.append("&") + break + case UnicodeScalar(UInt32(0xA0))!: + if (escapeMode != EscapeMode.xhtml) { + accum.append(" ") + } else { + accum.append(" ") + } + break + case UnicodeScalar.LessThan: + // escape when in character data or when in a xml attribue val; not needed in html attr val + if (!inAttribute || escapeMode == EscapeMode.xhtml) { + accum.append("<") + } else { + accum.append(c) + } + break + case UnicodeScalar.GreaterThan: + if (!inAttribute) { + accum.append(">") + } else { + accum.append(c)} + break + case "\"": + if (inAttribute) { + accum.append(""") + } else { + accum.append(c) + } + break + default: + if (canEncode(c, encoder)) { + accum.append(c) + } else { + appendEncoded(accum: accum, escapeMode: escapeMode, codePoint: codePoint) + } + } + } else { + if (encoder.canEncode(String(codePoint))) // uses fallback encoder for simplicity + { + accum.append(String(codePoint)) + } else { + appendEncoded(accum: accum, escapeMode: escapeMode, codePoint: codePoint) + } + } + } + } + + private static func appendEncoded(accum: StringBuilder, escapeMode: EscapeMode, codePoint: UnicodeScalar) { + if let name = escapeMode.nameForCodepoint(codePoint) { + // ok for identity check + accum.append(UnicodeScalar.Ampersand).append(name).append(";") + } else { + accum.append("&#x").append(String.toHexString(n: Int(codePoint.value)) ).append(";") + } + } + + public static func unescape(_ string: String)throws-> String { + return try unescape(string: string, strict: false) + } + + /** + * Unescape the input string. + * @param string to un-HTML-escape + * @param strict if "strict" (that is, requires trailing ';' char, otherwise that's optional) + * @return unescaped string + */ + public static func unescape(string: String, strict: Bool)throws -> String { + return try Parser.unescapeEntities(string, strict) + } + + /* + * Provides a fast-path for Encoder.canEncode, which drastically improves performance on Android post JellyBean. + * After KitKat, the implementation of canEncode degrades to the point of being useless. For non ASCII or UTF, + * performance may be bad. We can add more encoders for common character sets that are impacted by performance + * issues on Android if required. + * + * Benchmarks: * + * OLD toHtml() impl v New (fastpath) in millis + * Wiki: 1895, 16 + * CNN: 6378, 55 + * Alterslash: 3013, 28 + * Jsoup: 167, 2 + */ + private static func canEncode(_ c: UnicodeScalar, _ fallback: String.Encoding) -> Bool { + // todo add more charset tests if impacted by Android's bad perf in canEncode + switch (fallback) { + case String.Encoding.ascii: + return c.value < 0x80 + case String.Encoding.utf8: + return true // real is:!(Character.isLowSurrogate(c) || Character.isHighSurrogate(c)) - but already check above + default: + return fallback.canEncode(String(Character(c))) + } + } + + static let xhtml: String = "amp=12;1\ngt=1q;3\nlt=1o;2\nquot=y;0" + + static let base: String = "AElig=5i;1c\nAMP=12;2\nAacute=5d;17\nAcirc=5e;18\nAgrave=5c;16\nAring=5h;1b\nAtilde=5f;19\nAuml=5g;1a\nCOPY=4p;h\nCcedil=5j;1d\nETH=5s;1m\nEacute=5l;1f\nEcirc=5m;1g\nEgrave=5k;1e\nEuml=5n;1h\nGT=1q;6\nIacute=5p;1j\nIcirc=5q;1k\nIgrave=5o;1i\nIuml=5r;1l\nLT=1o;4\nNtilde=5t;1n\nOacute=5v;1p\nOcirc=5w;1q\nOgrave=5u;1o\nOslash=60;1u\nOtilde=5x;1r\nOuml=5y;1s\nQUOT=y;0\nREG=4u;n\nTHORN=66;20\nUacute=62;1w\nUcirc=63;1x\nUgrave=61;1v\nUuml=64;1y\nYacute=65;1z\naacute=69;23\nacirc=6a;24\nacute=50;u\naelig=6e;28\nagrave=68;22\namp=12;3\naring=6d;27\natilde=6b;25\nauml=6c;26\nbrvbar=4m;e\nccedil=6f;29\ncedil=54;y\ncent=4i;a\ncopy=4p;i\ncurren=4k;c\ndeg=4w;q\ndivide=6v;2p\neacute=6h;2b\necirc=6i;2c\negrave=6g;2a\neth=6o;2i\neuml=6j;2d\nfrac12=59;13\nfrac14=58;12\nfrac34=5a;14\ngt=1q;7\niacute=6l;2f\nicirc=6m;2g\niexcl=4h;9\nigrave=6k;2e\niquest=5b;15\niuml=6n;2h\nlaquo=4r;k\nlt=1o;5\nmacr=4v;p\nmicro=51;v\nmiddot=53;x\nnbsp=4g;8\nnot=4s;l\nntilde=6p;2j\noacute=6r;2l\nocirc=6s;2m\nograve=6q;2k\nordf=4q;j\nordm=56;10\noslash=6w;2q\notilde=6t;2n\nouml=6u;2o\npara=52;w\nplusmn=4x;r\npound=4j;b\nquot=y;1\nraquo=57;11\nreg=4u;o\nsect=4n;f\nshy=4t;m\nsup1=55;z\nsup2=4y;s\nsup3=4z;t\nszlig=67;21\nthorn=72;2w\ntimes=5z;1t\nuacute=6y;2s\nucirc=6z;2t\nugrave=6x;2r\numl=4o;g\nuuml=70;2u\nyacute=71;2v\nyen=4l;d\nyuml=73;2x" + + static let full: String = "AElig=5i;2v\nAMP=12;8\nAacute=5d;2p\nAbreve=76;4k\nAcirc=5e;2q\nAcy=sw;av\nAfr=2kn8;1kh\nAgrave=5c;2o\nAlpha=pd;8d\nAmacr=74;4i\nAnd=8cz;1e1\nAogon=78;4m\nAopf=2koo;1ls\nApplyFunction=6e9;ew\nAring=5h;2t\nAscr=2kkc;1jc\nAssign=6s4;s6\nAtilde=5f;2r\nAuml=5g;2s\nBackslash=6qe;o1\nBarv=8h3;1it\nBarwed=6x2;120\nBcy=sx;aw\nBecause=6r9;pw\nBernoullis=6jw;gn\nBeta=pe;8e\nBfr=2kn9;1ki\nBopf=2kop;1lt\nBreve=k8;82\nBscr=6jw;gp\nBumpeq=6ry;ro\nCHcy=tj;bi\nCOPY=4p;1q\nCacute=7a;4o\nCap=6vm;zz\nCapitalDifferentialD=6kl;h8\nCayleys=6jx;gq\nCcaron=7g;4u\nCcedil=5j;2w\nCcirc=7c;4q\nCconint=6r4;pn\nCdot=7e;4s\nCedilla=54;2e\nCenterDot=53;2b\nCfr=6jx;gr\nChi=pz;8y\nCircleDot=6u1;x8\nCircleMinus=6ty;x3\nCirclePlus=6tx;x1\nCircleTimes=6tz;x5\nClockwiseContourIntegral=6r6;pp\nCloseCurlyDoubleQuote=6cd;e0\nCloseCurlyQuote=6c9;dt\nColon=6rb;q1\nColone=8dw;1en\nCongruent=6sh;sn\nConint=6r3;pm\nContourIntegral=6r2;pi\nCopf=6iq;f7\nCoproduct=6q8;nq\nCounterClockwiseContourIntegral=6r7;pr\nCross=8bz;1d8\nCscr=2kke;1jd\nCup=6vn;100\nCupCap=6rx;rk\nDD=6kl;h9\nDDotrahd=841;184\nDJcy=si;ai\nDScy=sl;al\nDZcy=sv;au\nDagger=6ch;e7\nDarr=6n5;j5\nDashv=8h0;1ir\nDcaron=7i;4w\nDcy=t0;az\nDel=6pz;n9\nDelta=pg;8g\nDfr=2knb;1kj\nDiacriticalAcute=50;27\nDiacriticalDot=k9;84\nDiacriticalDoubleAcute=kd;8a\nDiacriticalGrave=2o;13\nDiacriticalTilde=kc;88\nDiamond=6v8;za\nDifferentialD=6km;ha\nDopf=2kor;1lu\nDot=4o;1n\nDotDot=6ho;f5\nDotEqual=6s0;rw\nDoubleContourIntegral=6r3;pl\nDoubleDot=4o;1m\nDoubleDownArrow=6oj;m0\nDoubleLeftArrow=6og;lq\nDoubleLeftRightArrow=6ok;m3\nDoubleLeftTee=8h0;1iq\nDoubleLongLeftArrow=7w8;17g\nDoubleLongLeftRightArrow=7wa;17m\nDoubleLongRightArrow=7w9;17j\nDoubleRightArrow=6oi;lw\nDoubleRightTee=6ug;xz\nDoubleUpArrow=6oh;lt\nDoubleUpDownArrow=6ol;m7\nDoubleVerticalBar=6qt;ov\nDownArrow=6mr;i8\nDownArrowBar=843;186\nDownArrowUpArrow=6ph;mn\nDownBreve=lt;8c\nDownLeftRightVector=85s;198\nDownLeftTeeVector=866;19m\nDownLeftVector=6nx;ke\nDownLeftVectorBar=85y;19e\nDownRightTeeVector=867;19n\nDownRightVector=6o1;kq\nDownRightVectorBar=85z;19f\nDownTee=6uc;xs\nDownTeeArrow=6nb;jh\nDownarrow=6oj;m1\nDscr=2kkf;1je\nDstrok=7k;4y\nENG=96;6g\nETH=5s;35\nEacute=5l;2y\nEcaron=7u;56\nEcirc=5m;2z\nEcy=tp;bo\nEdot=7q;52\nEfr=2knc;1kk\nEgrave=5k;2x\nElement=6q0;na\nEmacr=7m;50\nEmptySmallSquare=7i3;15x\nEmptyVerySmallSquare=7fv;150\nEogon=7s;54\nEopf=2kos;1lv\nEpsilon=ph;8h\nEqual=8dx;1eo\nEqualTilde=6rm;qp\nEquilibrium=6oc;li\nEscr=6k0;gu\nEsim=8dv;1em\nEta=pj;8j\nEuml=5n;30\nExists=6pv;mz\nExponentialE=6kn;hc\nFcy=tg;bf\nFfr=2knd;1kl\nFilledSmallSquare=7i4;15y\nFilledVerySmallSquare=7fu;14w\nFopf=2kot;1lw\nForAll=6ps;ms\nFouriertrf=6k1;gv\nFscr=6k1;gw\nGJcy=sj;aj\nGT=1q;r\nGamma=pf;8f\nGammad=rg;a5\nGbreve=7y;5a\nGcedil=82;5e\nGcirc=7w;58\nGcy=sz;ay\nGdot=80;5c\nGfr=2kne;1km\nGg=6vt;10c\nGopf=2kou;1lx\nGreaterEqual=6sl;sv\nGreaterEqualLess=6vv;10i\nGreaterFullEqual=6sn;t6\nGreaterGreater=8f6;1gh\nGreaterLess=6t3;ul\nGreaterSlantEqual=8e6;1f5\nGreaterTilde=6sz;ub\nGscr=2kki;1jf\nGt=6sr;tr\nHARDcy=tm;bl\nHacek=jr;80\nHat=2m;10\nHcirc=84;5f\nHfr=6j0;fe\nHilbertSpace=6iz;fa\nHopf=6j1;fg\nHorizontalLine=7b4;13i\nHscr=6iz;fc\nHstrok=86;5h\nHumpDownHump=6ry;rn\nHumpEqual=6rz;rs\nIEcy=t1;b0\nIJlig=8i;5s\nIOcy=sh;ah\nIacute=5p;32\nIcirc=5q;33\nIcy=t4;b3\nIdot=8g;5p\nIfr=6j5;fq\nIgrave=5o;31\nIm=6j5;fr\nImacr=8a;5l\nImaginaryI=6ko;hf\nImplies=6oi;ly\nInt=6r0;pf\nIntegral=6qz;pd\nIntersection=6v6;z4\nInvisibleComma=6eb;f0\nInvisibleTimes=6ea;ey\nIogon=8e;5n\nIopf=2kow;1ly\nIota=pl;8l\nIscr=6j4;fn\nItilde=88;5j\nIukcy=sm;am\nIuml=5r;34\nJcirc=8k;5u\nJcy=t5;b4\nJfr=2knh;1kn\nJopf=2kox;1lz\nJscr=2kkl;1jg\nJsercy=so;ao\nJukcy=sk;ak\nKHcy=th;bg\nKJcy=ss;as\nKappa=pm;8m\nKcedil=8m;5w\nKcy=t6;b5\nKfr=2kni;1ko\nKopf=2koy;1m0\nKscr=2kkm;1jh\nLJcy=sp;ap\nLT=1o;m\nLacute=8p;5z\nLambda=pn;8n\nLang=7vu;173\nLaplacetrf=6j6;fs\nLarr=6n2;j1\nLcaron=8t;63\nLcedil=8r;61\nLcy=t7;b6\nLeftAngleBracket=7vs;16x\nLeftArrow=6mo;hu\nLeftArrowBar=6p0;mj\nLeftArrowRightArrow=6o6;l3\nLeftCeiling=6x4;121\nLeftDoubleBracket=7vq;16t\nLeftDownTeeVector=869;19p\nLeftDownVector=6o3;kw\nLeftDownVectorBar=861;19h\nLeftFloor=6x6;125\nLeftRightArrow=6ms;ib\nLeftRightVector=85q;196\nLeftTee=6ub;xq\nLeftTeeArrow=6n8;ja\nLeftTeeVector=862;19i\nLeftTriangle=6uq;ya\nLeftTriangleBar=89b;1c0\nLeftTriangleEqual=6us;yg\nLeftUpDownVector=85t;199\nLeftUpTeeVector=868;19o\nLeftUpVector=6nz;kk\nLeftUpVectorBar=860;19g\nLeftVector=6nw;kb\nLeftVectorBar=85u;19a\nLeftarrow=6og;lr\nLeftrightarrow=6ok;m4\nLessEqualGreater=6vu;10e\nLessFullEqual=6sm;t0\nLessGreater=6t2;ui\nLessLess=8f5;1gf\nLessSlantEqual=8e5;1ez\nLessTilde=6sy;u8\nLfr=2knj;1kp\nLl=6vs;109\nLleftarrow=6oq;me\nLmidot=8v;65\nLongLeftArrow=7w5;177\nLongLeftRightArrow=7w7;17d\nLongRightArrow=7w6;17a\nLongleftarrow=7w8;17h\nLongleftrightarrow=7wa;17n\nLongrightarrow=7w9;17k\nLopf=2koz;1m1\nLowerLeftArrow=6mx;iq\nLowerRightArrow=6mw;in\nLscr=6j6;fu\nLsh=6nk;jv\nLstrok=8x;67\nLt=6sq;tl\nMap=83p;17v\nMcy=t8;b7\nMediumSpace=6e7;eu\nMellintrf=6k3;gx\nMfr=2knk;1kq\nMinusPlus=6qb;nv\nMopf=2kp0;1m2\nMscr=6k3;gz\nMu=po;8o\nNJcy=sq;aq\nNacute=8z;69\nNcaron=93;6d\nNcedil=91;6b\nNcy=t9;b8\nNegativeMediumSpace=6bv;dc\nNegativeThickSpace=6bv;dd\nNegativeThinSpace=6bv;de\nNegativeVeryThinSpace=6bv;db\nNestedGreaterGreater=6sr;tq\nNestedLessLess=6sq;tk\nNewLine=a;1\nNfr=2knl;1kr\nNoBreak=6e8;ev\nNonBreakingSpace=4g;1d\nNopf=6j9;fx\nNot=8h8;1ix\nNotCongruent=6si;sp\nNotCupCap=6st;tv\nNotDoubleVerticalBar=6qu;p0\nNotElement=6q1;ne\nNotEqual=6sg;sk\nNotEqualTilde=6rm,mw;qn\nNotExists=6pw;n1\nNotGreater=6sv;tz\nNotGreaterEqual=6sx;u5\nNotGreaterFullEqual=6sn,mw;t3\nNotGreaterGreater=6sr,mw;tn\nNotGreaterLess=6t5;uq\nNotGreaterSlantEqual=8e6,mw;1f2\nNotGreaterTilde=6t1;ug\nNotHumpDownHump=6ry,mw;rl\nNotHumpEqual=6rz,mw;rq\nNotLeftTriangle=6wa;113\nNotLeftTriangleBar=89b,mw;1bz\nNotLeftTriangleEqual=6wc;119\nNotLess=6su;tw\nNotLessEqual=6sw;u2\nNotLessGreater=6t4;uo\nNotLessLess=6sq,mw;th\nNotLessSlantEqual=8e5,mw;1ew\nNotLessTilde=6t0;ue\nNotNestedGreaterGreater=8f6,mw;1gg\nNotNestedLessLess=8f5,mw;1ge\nNotPrecedes=6tc;vb\nNotPrecedesEqual=8fj,mw;1gv\nNotPrecedesSlantEqual=6w0;10p\nNotReverseElement=6q4;nl\nNotRightTriangle=6wb;116\nNotRightTriangleBar=89c,mw;1c1\nNotRightTriangleEqual=6wd;11c\nNotSquareSubset=6tr,mw;wh\nNotSquareSubsetEqual=6w2;10t\nNotSquareSuperset=6ts,mw;wl\nNotSquareSupersetEqual=6w3;10v\nNotSubset=6te,6he;vh\nNotSubsetEqual=6tk;w0\nNotSucceeds=6td;ve\nNotSucceedsEqual=8fk,mw;1h1\nNotSucceedsSlantEqual=6w1;10r\nNotSucceedsTilde=6tb,mw;v7\nNotSuperset=6tf,6he;vm\nNotSupersetEqual=6tl;w3\nNotTilde=6rl;ql\nNotTildeEqual=6ro;qv\nNotTildeFullEqual=6rr;r1\nNotTildeTilde=6rt;r9\nNotVerticalBar=6qs;or\nNscr=2kkp;1ji\nNtilde=5t;36\nNu=pp;8p\nOElig=9e;6m\nOacute=5v;38\nOcirc=5w;39\nOcy=ta;b9\nOdblac=9c;6k\nOfr=2knm;1ks\nOgrave=5u;37\nOmacr=98;6i\nOmega=q1;90\nOmicron=pr;8r\nOopf=2kp2;1m3\nOpenCurlyDoubleQuote=6cc;dy\nOpenCurlyQuote=6c8;dr\nOr=8d0;1e2\nOscr=2kkq;1jj\nOslash=60;3d\nOtilde=5x;3a\nOtimes=8c7;1df\nOuml=5y;3b\nOverBar=6da;em\nOverBrace=732;13b\nOverBracket=71w;134\nOverParenthesis=730;139\nPartialD=6pu;mx\nPcy=tb;ba\nPfr=2knn;1kt\nPhi=py;8x\nPi=ps;8s\nPlusMinus=4x;22\nPoincareplane=6j0;fd\nPopf=6jd;g3\nPr=8fv;1hl\nPrecedes=6t6;us\nPrecedesEqual=8fj;1gy\nPrecedesSlantEqual=6t8;uy\nPrecedesTilde=6ta;v4\nPrime=6cz;eg\nProduct=6q7;no\nProportion=6rb;q0\nProportional=6ql;oa\nPscr=2kkr;1jk\nPsi=q0;8z\nQUOT=y;3\nQfr=2kno;1ku\nQopf=6je;g5\nQscr=2kks;1jl\nRBarr=840;183\nREG=4u;1x\nRacute=9g;6o\nRang=7vv;174\nRarr=6n4;j4\nRarrtl=846;187\nRcaron=9k;6s\nRcedil=9i;6q\nRcy=tc;bb\nRe=6jg;gb\nReverseElement=6q3;nh\nReverseEquilibrium=6ob;le\nReverseUpEquilibrium=86n;1a4\nRfr=6jg;ga\nRho=pt;8t\nRightAngleBracket=7vt;170\nRightArrow=6mq;i3\nRightArrowBar=6p1;ml\nRightArrowLeftArrow=6o4;ky\nRightCeiling=6x5;123\nRightDoubleBracket=7vr;16v\nRightDownTeeVector=865;19l\nRightDownVector=6o2;kt\nRightDownVectorBar=85x;19d\nRightFloor=6x7;127\nRightTee=6ua;xo\nRightTeeArrow=6na;je\nRightTeeVector=863;19j\nRightTriangle=6ur;yd\nRightTriangleBar=89c;1c2\nRightTriangleEqual=6ut;yk\nRightUpDownVector=85r;197\nRightUpTeeVector=864;19k\nRightUpVector=6ny;kh\nRightUpVectorBar=85w;19c\nRightVector=6o0;kn\nRightVectorBar=85v;19b\nRightarrow=6oi;lx\nRopf=6jh;gd\nRoundImplies=86o;1a6\nRrightarrow=6or;mg\nRscr=6jf;g7\nRsh=6nl;jx\nRuleDelayed=8ac;1cb\nSHCHcy=tl;bk\nSHcy=tk;bj\nSOFTcy=to;bn\nSacute=9m;6u\nSc=8fw;1hm\nScaron=9s;70\nScedil=9q;6y\nScirc=9o;6w\nScy=td;bc\nSfr=2knq;1kv\nShortDownArrow=6mr;i7\nShortLeftArrow=6mo;ht\nShortRightArrow=6mq;i2\nShortUpArrow=6mp;hy\nSigma=pv;8u\nSmallCircle=6qg;o6\nSopf=2kp6;1m4\nSqrt=6qi;o9\nSquare=7fl;14t\nSquareIntersection=6tv;ww\nSquareSubset=6tr;wi\nSquareSubsetEqual=6tt;wp\nSquareSuperset=6ts;wm\nSquareSupersetEqual=6tu;ws\nSquareUnion=6tw;wz\nSscr=2kku;1jm\nStar=6va;zf\nSub=6vk;zw\nSubset=6vk;zv\nSubsetEqual=6ti;vu\nSucceeds=6t7;uv\nSucceedsEqual=8fk;1h4\nSucceedsSlantEqual=6t9;v1\nSucceedsTilde=6tb;v8\nSuchThat=6q3;ni\nSum=6q9;ns\nSup=6vl;zy\nSuperset=6tf;vp\nSupersetEqual=6tj;vx\nSupset=6vl;zx\nTHORN=66;3j\nTRADE=6jm;gf\nTSHcy=sr;ar\nTScy=ti;bh\nTab=9;0\nTau=pw;8v\nTcaron=9w;74\nTcedil=9u;72\nTcy=te;bd\nTfr=2knr;1kw\nTherefore=6r8;pt\nTheta=pk;8k\nThickSpace=6e7,6bu;et\nThinSpace=6bt;d7\nTilde=6rg;q9\nTildeEqual=6rn;qs\nTildeFullEqual=6rp;qy\nTildeTilde=6rs;r4\nTopf=2kp7;1m5\nTripleDot=6hn;f3\nTscr=2kkv;1jn\nTstrok=9y;76\nUacute=62;3f\nUarr=6n3;j2\nUarrocir=85l;193\nUbrcy=su;at\nUbreve=a4;7c\nUcirc=63;3g\nUcy=tf;be\nUdblac=a8;7g\nUfr=2kns;1kx\nUgrave=61;3e\nUmacr=a2;7a\nUnderBar=2n;11\nUnderBrace=733;13c\nUnderBracket=71x;136\nUnderParenthesis=731;13a\nUnion=6v7;z8\nUnionPlus=6tq;wf\nUogon=aa;7i\nUopf=2kp8;1m6\nUpArrow=6mp;hz\nUpArrowBar=842;185\nUpArrowDownArrow=6o5;l1\nUpDownArrow=6mt;ie\nUpEquilibrium=86m;1a2\nUpTee=6ud;xv\nUpTeeArrow=6n9;jc\nUparrow=6oh;lu\nUpdownarrow=6ol;m8\nUpperLeftArrow=6mu;ih\nUpperRightArrow=6mv;ik\nUpsi=r6;9z\nUpsilon=px;8w\nUring=a6;7e\nUscr=2kkw;1jo\nUtilde=a0;78\nUuml=64;3h\nVDash=6uj;y3\nVbar=8h7;1iw\nVcy=sy;ax\nVdash=6uh;y1\nVdashl=8h2;1is\nVee=6v5;z3\nVerbar=6c6;dp\nVert=6c6;dq\nVerticalBar=6qr;on\nVerticalLine=3g;18\nVerticalSeparator=7rs;16o\nVerticalTilde=6rk;qi\nVeryThinSpace=6bu;d9\nVfr=2knt;1ky\nVopf=2kp9;1m7\nVscr=2kkx;1jp\nVvdash=6ui;y2\nWcirc=ac;7k\nWedge=6v4;z0\nWfr=2knu;1kz\nWopf=2kpa;1m8\nWscr=2kky;1jq\nXfr=2knv;1l0\nXi=pq;8q\nXopf=2kpb;1m9\nXscr=2kkz;1jr\nYAcy=tr;bq\nYIcy=sn;an\nYUcy=tq;bp\nYacute=65;3i\nYcirc=ae;7m\nYcy=tn;bm\nYfr=2knw;1l1\nYopf=2kpc;1ma\nYscr=2kl0;1js\nYuml=ag;7o\nZHcy=t2;b1\nZacute=ah;7p\nZcaron=al;7t\nZcy=t3;b2\nZdot=aj;7r\nZeroWidthSpace=6bv;df\nZeta=pi;8i\nZfr=6js;gl\nZopf=6jo;gi\nZscr=2kl1;1jt\naacute=69;3m\nabreve=77;4l\nac=6ri;qg\nacE=6ri,mr;qe\nacd=6rj;qh\nacirc=6a;3n\nacute=50;28\nacy=ts;br\naelig=6e;3r\naf=6e9;ex\nafr=2kny;1l2\nagrave=68;3l\nalefsym=6k5;h3\naleph=6k5;h4\nalpha=q9;92\namacr=75;4j\namalg=8cf;1dm\namp=12;9\nand=6qv;p6\nandand=8d1;1e3\nandd=8d8;1e9\nandslope=8d4;1e6\nandv=8d6;1e7\nang=6qo;oj\nange=884;1b1\nangle=6qo;oi\nangmsd=6qp;ol\nangmsdaa=888;1b5\nangmsdab=889;1b6\nangmsdac=88a;1b7\nangmsdad=88b;1b8\nangmsdae=88c;1b9\nangmsdaf=88d;1ba\nangmsdag=88e;1bb\nangmsdah=88f;1bc\nangrt=6qn;og\nangrtvb=6v2;yw\nangrtvbd=87x;1b0\nangsph=6qq;om\nangst=5h;2u\nangzarr=70c;12z\naogon=79;4n\naopf=2kpe;1mb\nap=6rs;r8\napE=8ds;1ej\napacir=8dr;1eh\nape=6ru;rd\napid=6rv;rf\napos=13;a\napprox=6rs;r5\napproxeq=6ru;rc\naring=6d;3q\nascr=2kl2;1ju\nast=16;e\nasymp=6rs;r6\nasympeq=6rx;rj\natilde=6b;3o\nauml=6c;3p\nawconint=6r7;ps\nawint=8b5;1cr\nbNot=8h9;1iy\nbackcong=6rw;rg\nbackepsilon=s6;af\nbackprime=6d1;ei\nbacksim=6rh;qc\nbacksimeq=6vh;zp\nbarvee=6v1;yv\nbarwed=6x1;11y\nbarwedge=6x1;11x\nbbrk=71x;137\nbbrktbrk=71y;138\nbcong=6rw;rh\nbcy=tt;bs\nbdquo=6ce;e4\nbecaus=6r9;py\nbecause=6r9;px\nbemptyv=88g;1bd\nbepsi=s6;ag\nbernou=6jw;go\nbeta=qa;93\nbeth=6k6;h5\nbetween=6ss;tt\nbfr=2knz;1l3\nbigcap=6v6;z5\nbigcirc=7hr;15s\nbigcup=6v7;z7\nbigodot=8ao;1cd\nbigoplus=8ap;1cf\nbigotimes=8aq;1ch\nbigsqcup=8au;1cl\nbigstar=7id;15z\nbigtriangledown=7gd;15e\nbigtriangleup=7g3;154\nbiguplus=8as;1cj\nbigvee=6v5;z1\nbigwedge=6v4;yy\nbkarow=83x;17x\nblacklozenge=8a3;1c9\nblacksquare=7fu;14x\nblacktriangle=7g4;156\nblacktriangledown=7ge;15g\nblacktriangleleft=7gi;15k\nblacktriangleright=7g8;15a\nblank=74z;13f\nblk12=7f6;14r\nblk14=7f5;14q\nblk34=7f7;14s\nblock=7ew;14p\nbne=1p,6hx;o\nbnequiv=6sh,6hx;sm\nbnot=6xc;12d\nbopf=2kpf;1mc\nbot=6ud;xx\nbottom=6ud;xu\nbowtie=6vc;zi\nboxDL=7dj;141\nboxDR=7dg;13y\nboxDl=7di;140\nboxDr=7df;13x\nboxH=7dc;13u\nboxHD=7dy;14g\nboxHU=7e1;14j\nboxHd=7dw;14e\nboxHu=7dz;14h\nboxUL=7dp;147\nboxUR=7dm;144\nboxUl=7do;146\nboxUr=7dl;143\nboxV=7dd;13v\nboxVH=7e4;14m\nboxVL=7dv;14d\nboxVR=7ds;14a\nboxVh=7e3;14l\nboxVl=7du;14c\nboxVr=7dr;149\nboxbox=895;1bw\nboxdL=7dh;13z\nboxdR=7de;13w\nboxdl=7bk;13m\nboxdr=7bg;13l\nboxh=7b4;13j\nboxhD=7dx;14f\nboxhU=7e0;14i\nboxhd=7cc;13r\nboxhu=7ck;13s\nboxminus=6u7;xi\nboxplus=6u6;xg\nboxtimes=6u8;xk\nboxuL=7dn;145\nboxuR=7dk;142\nboxul=7bs;13o\nboxur=7bo;13n\nboxv=7b6;13k\nboxvH=7e2;14k\nboxvL=7dt;14b\nboxvR=7dq;148\nboxvh=7cs;13t\nboxvl=7c4;13q\nboxvr=7bw;13p\nbprime=6d1;ej\nbreve=k8;83\nbrvbar=4m;1k\nbscr=2kl3;1jv\nbsemi=6dr;er\nbsim=6rh;qd\nbsime=6vh;zq\nbsol=2k;x\nbsolb=891;1bv\nbsolhsub=7uw;16r\nbull=6ci;e9\nbullet=6ci;e8\nbump=6ry;rp\nbumpE=8fi;1gu\nbumpe=6rz;ru\nbumpeq=6rz;rt\ncacute=7b;4p\ncap=6qx;pa\ncapand=8ck;1dq\ncapbrcup=8cp;1dv\ncapcap=8cr;1dx\ncapcup=8cn;1dt\ncapdot=8cg;1dn\ncaps=6qx,1e68;p9\ncaret=6dd;eo\ncaron=jr;81\nccaps=8ct;1dz\nccaron=7h;4v\nccedil=6f;3s\nccirc=7d;4r\nccups=8cs;1dy\nccupssm=8cw;1e0\ncdot=7f;4t\ncedil=54;2f\ncemptyv=88i;1bf\ncent=4i;1g\ncenterdot=53;2c\ncfr=2ko0;1l4\nchcy=uf;ce\ncheck=7pv;16j\ncheckmark=7pv;16i\nchi=qv;9s\ncir=7gr;15q\ncirE=88z;1bt\ncirc=jq;7z\ncirceq=6s7;sc\ncirclearrowleft=6nu;k6\ncirclearrowright=6nv;k8\ncircledR=4u;1w\ncircledS=79k;13g\ncircledast=6u3;xc\ncircledcirc=6u2;xa\ncircleddash=6u5;xe\ncire=6s7;sd\ncirfnint=8b4;1cq\ncirmid=8hb;1j0\ncirscir=88y;1bs\nclubs=7kz;168\nclubsuit=7kz;167\ncolon=1m;j\ncolone=6s4;s7\ncoloneq=6s4;s5\ncomma=18;g\ncommat=1s;u\ncomp=6pt;mv\ncompfn=6qg;o7\ncomplement=6pt;mu\ncomplexes=6iq;f6\ncong=6rp;qz\ncongdot=8dp;1ef\nconint=6r2;pj\ncopf=2kpg;1md\ncoprod=6q8;nr\ncopy=4p;1r\ncopysr=6jb;fz\ncrarr=6np;k1\ncross=7pz;16k\ncscr=2kl4;1jw\ncsub=8gf;1id\ncsube=8gh;1if\ncsup=8gg;1ie\ncsupe=8gi;1ig\nctdot=6wf;11g\ncudarrl=854;18x\ncudarrr=851;18u\ncuepr=6vy;10m\ncuesc=6vz;10o\ncularr=6nq;k3\ncularrp=859;190\ncup=6qy;pc\ncupbrcap=8co;1du\ncupcap=8cm;1ds\ncupcup=8cq;1dw\ncupdot=6tp;we\ncupor=8cl;1dr\ncups=6qy,1e68;pb\ncurarr=6nr;k5\ncurarrm=858;18z\ncurlyeqprec=6vy;10l\ncurlyeqsucc=6vz;10n\ncurlyvee=6vi;zr\ncurlywedge=6vj;zt\ncurren=4k;1i\ncurvearrowleft=6nq;k2\ncurvearrowright=6nr;k4\ncuvee=6vi;zs\ncuwed=6vj;zu\ncwconint=6r6;pq\ncwint=6r5;po\ncylcty=6y5;12u\ndArr=6oj;m2\ndHar=86d;19t\ndagger=6cg;e5\ndaleth=6k8;h7\ndarr=6mr;ia\ndash=6c0;dl\ndashv=6ub;xr\ndbkarow=83z;180\ndblac=kd;8b\ndcaron=7j;4x\ndcy=tw;bv\ndd=6km;hb\nddagger=6ch;e6\nddarr=6oa;ld\nddotseq=8dz;1ep\ndeg=4w;21\ndelta=qc;95\ndemptyv=88h;1be\ndfisht=873;1aj\ndfr=2ko1;1l5\ndharl=6o3;kx\ndharr=6o2;ku\ndiam=6v8;zc\ndiamond=6v8;zb\ndiamondsuit=7l2;16b\ndiams=7l2;16c\ndie=4o;1o\ndigamma=rh;a6\ndisin=6wi;11j\ndiv=6v;49\ndivide=6v;48\ndivideontimes=6vb;zg\ndivonx=6vb;zh\ndjcy=uq;co\ndlcorn=6xq;12n\ndlcrop=6x9;12a\ndollar=10;6\ndopf=2kph;1me\ndot=k9;85\ndoteq=6s0;rx\ndoteqdot=6s1;rz\ndotminus=6rc;q2\ndotplus=6qc;ny\ndotsquare=6u9;xm\ndoublebarwedge=6x2;11z\ndownarrow=6mr;i9\ndowndownarrows=6oa;lc\ndownharpoonleft=6o3;kv\ndownharpoonright=6o2;ks\ndrbkarow=840;182\ndrcorn=6xr;12p\ndrcrop=6x8;129\ndscr=2kl5;1jx\ndscy=ut;cr\ndsol=8ae;1cc\ndstrok=7l;4z\ndtdot=6wh;11i\ndtri=7gf;15j\ndtrif=7ge;15h\nduarr=6ph;mo\nduhar=86n;1a5\ndwangle=886;1b3\ndzcy=v3;d0\ndzigrarr=7wf;17r\neDDot=8dz;1eq\neDot=6s1;s0\neacute=6h;3u\neaster=8dq;1eg\necaron=7v;57\necir=6s6;sb\necirc=6i;3v\necolon=6s5;s9\necy=ul;ck\nedot=7r;53\nee=6kn;he\nefDot=6s2;s2\nefr=2ko2;1l6\neg=8ey;1g9\negrave=6g;3t\negs=8eu;1g5\negsdot=8ew;1g7\nel=8ex;1g8\nelinters=73b;13e\nell=6j7;fv\nels=8et;1g3\nelsdot=8ev;1g6\nemacr=7n;51\nempty=6px;n7\nemptyset=6px;n5\nemptyv=6px;n6\nemsp=6bn;d2\nemsp13=6bo;d3\nemsp14=6bp;d4\neng=97;6h\nensp=6bm;d1\neogon=7t;55\neopf=2kpi;1mf\nepar=6vp;103\neparsl=89v;1c6\neplus=8dt;1ek\nepsi=qd;97\nepsilon=qd;96\nepsiv=s5;ae\neqcirc=6s6;sa\neqcolon=6s5;s8\neqsim=6rm;qq\neqslantgtr=8eu;1g4\neqslantless=8et;1g2\nequals=1p;p\nequest=6sf;sj\nequiv=6sh;so\nequivDD=8e0;1er\neqvparsl=89x;1c8\nerDot=6s3;s4\nerarr=86p;1a7\nescr=6jz;gs\nesdot=6s0;ry\nesim=6rm;qr\neta=qf;99\neth=6o;41\neuml=6j;3w\neuro=6gc;f2\nexcl=x;2\nexist=6pv;n0\nexpectation=6k0;gt\nexponentiale=6kn;hd\nfallingdotseq=6s2;s1\nfcy=uc;cb\nfemale=7k0;163\nffilig=1dkz;1ja\nfflig=1dkw;1j7\nffllig=1dl0;1jb\nffr=2ko3;1l7\nfilig=1dkx;1j8\nfjlig=2u,2y;15\nflat=7l9;16e\nfllig=1dky;1j9\nfltns=7g1;153\nfnof=b6;7v\nfopf=2kpj;1mg\nforall=6ps;mt\nfork=6vo;102\nforkv=8gp;1in\nfpartint=8b1;1cp\nfrac12=59;2k\nfrac13=6kz;hh\nfrac14=58;2j\nfrac15=6l1;hj\nfrac16=6l5;hn\nfrac18=6l7;hp\nfrac23=6l0;hi\nfrac25=6l2;hk\nfrac34=5a;2m\nfrac35=6l3;hl\nfrac38=6l8;hq\nfrac45=6l4;hm\nfrac56=6l6;ho\nfrac58=6l9;hr\nfrac78=6la;hs\nfrasl=6dg;eq\nfrown=6xu;12r\nfscr=2kl7;1jy\ngE=6sn;t8\ngEl=8ek;1ft\ngacute=dx;7x\ngamma=qb;94\ngammad=rh;a7\ngap=8ee;1fh\ngbreve=7z;5b\ngcirc=7x;59\ngcy=tv;bu\ngdot=81;5d\nge=6sl;sx\ngel=6vv;10k\ngeq=6sl;sw\ngeqq=6sn;t7\ngeqslant=8e6;1f6\nges=8e6;1f7\ngescc=8fd;1gn\ngesdot=8e8;1f9\ngesdoto=8ea;1fb\ngesdotol=8ec;1fd\ngesl=6vv,1e68;10h\ngesles=8es;1g1\ngfr=2ko4;1l8\ngg=6sr;ts\nggg=6vt;10b\ngimel=6k7;h6\ngjcy=ur;cp\ngl=6t3;un\nglE=8eq;1fz\ngla=8f9;1gj\nglj=8f8;1gi\ngnE=6sp;tg\ngnap=8ei;1fp\ngnapprox=8ei;1fo\ngne=8eg;1fl\ngneq=8eg;1fk\ngneqq=6sp;tf\ngnsim=6w7;10y\ngopf=2kpk;1mh\ngrave=2o;14\ngscr=6iy;f9\ngsim=6sz;ud\ngsime=8em;1fv\ngsiml=8eo;1fx\ngt=1q;s\ngtcc=8fb;1gl\ngtcir=8e2;1et\ngtdot=6vr;107\ngtlPar=87p;1aw\ngtquest=8e4;1ev\ngtrapprox=8ee;1fg\ngtrarr=86w;1ad\ngtrdot=6vr;106\ngtreqless=6vv;10j\ngtreqqless=8ek;1fs\ngtrless=6t3;um\ngtrsim=6sz;uc\ngvertneqq=6sp,1e68;td\ngvnE=6sp,1e68;te\nhArr=6ok;m5\nhairsp=6bu;da\nhalf=59;2l\nhamilt=6iz;fb\nhardcy=ui;ch\nharr=6ms;id\nharrcir=85k;192\nharrw=6nh;js\nhbar=6j3;fl\nhcirc=85;5g\nhearts=7l1;16a\nheartsuit=7l1;169\nhellip=6cm;eb\nhercon=6ux;yr\nhfr=2ko5;1l9\nhksearow=84l;18i\nhkswarow=84m;18k\nhoarr=6pr;mr\nhomtht=6rf;q5\nhookleftarrow=6nd;jj\nhookrightarrow=6ne;jl\nhopf=2kpl;1mi\nhorbar=6c5;do\nhscr=2kl9;1jz\nhslash=6j3;fi\nhstrok=87;5i\nhybull=6df;ep\nhyphen=6c0;dk\niacute=6l;3y\nic=6eb;f1\nicirc=6m;3z\nicy=u0;bz\niecy=tx;bw\niexcl=4h;1f\niff=6ok;m6\nifr=2ko6;1la\nigrave=6k;3x\nii=6ko;hg\niiiint=8b0;1cn\niiint=6r1;pg\niinfin=89o;1c3\niiota=6jt;gm\nijlig=8j;5t\nimacr=8b;5m\nimage=6j5;fp\nimagline=6j4;fm\nimagpart=6j5;fo\nimath=8h;5r\nimof=6uv;yo\nimped=c5;7w\nin=6q0;nd\nincare=6it;f8\ninfin=6qm;of\ninfintie=89p;1c4\ninodot=8h;5q\nint=6qz;pe\nintcal=6uy;yt\nintegers=6jo;gh\nintercal=6uy;ys\nintlarhk=8bb;1cx\nintprod=8cc;1dk\niocy=up;cn\niogon=8f;5o\niopf=2kpm;1mj\niota=qh;9b\niprod=8cc;1dl\niquest=5b;2n\niscr=2kla;1k0\nisin=6q0;nc\nisinE=6wp;11r\nisindot=6wl;11n\nisins=6wk;11l\nisinsv=6wj;11k\nisinv=6q0;nb\nit=6ea;ez\nitilde=89;5k\niukcy=uu;cs\niuml=6n;40\njcirc=8l;5v\njcy=u1;c0\njfr=2ko7;1lb\njmath=fr;7y\njopf=2kpn;1mk\njscr=2klb;1k1\njsercy=uw;cu\njukcy=us;cq\nkappa=qi;9c\nkappav=s0;a9\nkcedil=8n;5x\nkcy=u2;c1\nkfr=2ko8;1lc\nkgreen=8o;5y\nkhcy=ud;cc\nkjcy=v0;cy\nkopf=2kpo;1ml\nkscr=2klc;1k2\nlAarr=6oq;mf\nlArr=6og;ls\nlAtail=84b;18a\nlBarr=83y;17z\nlE=6sm;t2\nlEg=8ej;1fr\nlHar=86a;19q\nlacute=8q;60\nlaemptyv=88k;1bh\nlagran=6j6;ft\nlambda=qj;9d\nlang=7vs;16z\nlangd=87l;1as\nlangle=7vs;16y\nlap=8ed;1ff\nlaquo=4r;1t\nlarr=6mo;hx\nlarrb=6p0;mk\nlarrbfs=84f;18e\nlarrfs=84d;18c\nlarrhk=6nd;jk\nlarrlp=6nf;jo\nlarrpl=855;18y\nlarrsim=86r;1a9\nlarrtl=6n6;j7\nlat=8ff;1gp\nlatail=849;188\nlate=8fh;1gt\nlates=8fh,1e68;1gs\nlbarr=83w;17w\nlbbrk=7si;16p\nlbrace=3f;16\nlbrack=2j;v\nlbrke=87f;1am\nlbrksld=87j;1aq\nlbrkslu=87h;1ao\nlcaron=8u;64\nlcedil=8s;62\nlceil=6x4;122\nlcub=3f;17\nlcy=u3;c2\nldca=852;18v\nldquo=6cc;dz\nldquor=6ce;e3\nldrdhar=86f;19v\nldrushar=85n;195\nldsh=6nm;jz\nle=6sk;st\nleftarrow=6mo;hv\nleftarrowtail=6n6;j6\nleftharpoondown=6nx;kd\nleftharpoonup=6nw;ka\nleftleftarrows=6o7;l6\nleftrightarrow=6ms;ic\nleftrightarrows=6o6;l4\nleftrightharpoons=6ob;lf\nleftrightsquigarrow=6nh;jr\nleftthreetimes=6vf;zl\nleg=6vu;10g\nleq=6sk;ss\nleqq=6sm;t1\nleqslant=8e5;1f0\nles=8e5;1f1\nlescc=8fc;1gm\nlesdot=8e7;1f8\nlesdoto=8e9;1fa\nlesdotor=8eb;1fc\nlesg=6vu,1e68;10d\nlesges=8er;1g0\nlessapprox=8ed;1fe\nlessdot=6vq;104\nlesseqgtr=6vu;10f\nlesseqqgtr=8ej;1fq\nlessgtr=6t2;uj\nlesssim=6sy;u9\nlfisht=870;1ag\nlfloor=6x6;126\nlfr=2ko9;1ld\nlg=6t2;uk\nlgE=8ep;1fy\nlhard=6nx;kf\nlharu=6nw;kc\nlharul=86i;19y\nlhblk=7es;14o\nljcy=ux;cv\nll=6sq;tm\nllarr=6o7;l7\nllcorner=6xq;12m\nllhard=86j;19z\nlltri=7i2;15w\nlmidot=8w;66\nlmoust=71s;131\nlmoustache=71s;130\nlnE=6so;tc\nlnap=8eh;1fn\nlnapprox=8eh;1fm\nlne=8ef;1fj\nlneq=8ef;1fi\nlneqq=6so;tb\nlnsim=6w6;10x\nloang=7vw;175\nloarr=6pp;mp\nlobrk=7vq;16u\nlongleftarrow=7w5;178\nlongleftrightarrow=7w7;17e\nlongmapsto=7wc;17p\nlongrightarrow=7w6;17b\nlooparrowleft=6nf;jn\nlooparrowright=6ng;jp\nlopar=879;1ak\nlopf=2kpp;1mm\nloplus=8bx;1d6\nlotimes=8c4;1dc\nlowast=6qf;o5\nlowbar=2n;12\nloz=7gq;15p\nlozenge=7gq;15o\nlozf=8a3;1ca\nlpar=14;b\nlparlt=87n;1au\nlrarr=6o6;l5\nlrcorner=6xr;12o\nlrhar=6ob;lg\nlrhard=86l;1a1\nlrm=6by;di\nlrtri=6v3;yx\nlsaquo=6d5;ek\nlscr=2kld;1k3\nlsh=6nk;jw\nlsim=6sy;ua\nlsime=8el;1fu\nlsimg=8en;1fw\nlsqb=2j;w\nlsquo=6c8;ds\nlsquor=6ca;dw\nlstrok=8y;68\nlt=1o;n\nltcc=8fa;1gk\nltcir=8e1;1es\nltdot=6vq;105\nlthree=6vf;zm\nltimes=6vd;zj\nltlarr=86u;1ac\nltquest=8e3;1eu\nltrPar=87q;1ax\nltri=7gj;15n\nltrie=6us;yi\nltrif=7gi;15l\nlurdshar=85m;194\nluruhar=86e;19u\nlvertneqq=6so,1e68;t9\nlvnE=6so,1e68;ta\nmDDot=6re;q4\nmacr=4v;20\nmale=7k2;164\nmalt=7q8;16m\nmaltese=7q8;16l\nmap=6na;jg\nmapsto=6na;jf\nmapstodown=6nb;ji\nmapstoleft=6n8;jb\nmapstoup=6n9;jd\nmarker=7fy;152\nmcomma=8bt;1d4\nmcy=u4;c3\nmdash=6c4;dn\nmeasuredangle=6qp;ok\nmfr=2koa;1le\nmho=6jr;gj\nmicro=51;29\nmid=6qr;oq\nmidast=16;d\nmidcir=8hc;1j1\nmiddot=53;2d\nminus=6qa;nu\nminusb=6u7;xj\nminusd=6rc;q3\nminusdu=8bu;1d5\nmlcp=8gr;1ip\nmldr=6cm;ec\nmnplus=6qb;nw\nmodels=6uf;xy\nmopf=2kpq;1mn\nmp=6qb;nx\nmscr=2kle;1k4\nmstpos=6ri;qf\nmu=qk;9e\nmultimap=6uw;yp\nmumap=6uw;yq\nnGg=6vt,mw;10a\nnGt=6sr,6he;tp\nnGtv=6sr,mw;to\nnLeftarrow=6od;lk\nnLeftrightarrow=6oe;lm\nnLl=6vs,mw;108\nnLt=6sq,6he;tj\nnLtv=6sq,mw;ti\nnRightarrow=6of;lo\nnVDash=6un;y7\nnVdash=6um;y6\nnabla=6pz;n8\nnacute=90;6a\nnang=6qo,6he;oh\nnap=6rt;rb\nnapE=8ds,mw;1ei\nnapid=6rv,mw;re\nnapos=95;6f\nnapprox=6rt;ra\nnatur=7la;16g\nnatural=7la;16f\nnaturals=6j9;fw\nnbsp=4g;1e\nnbump=6ry,mw;rm\nnbumpe=6rz,mw;rr\nncap=8cj;1dp\nncaron=94;6e\nncedil=92;6c\nncong=6rr;r2\nncongdot=8dp,mw;1ee\nncup=8ci;1do\nncy=u5;c4\nndash=6c3;dm\nne=6sg;sl\nneArr=6on;mb\nnearhk=84k;18h\nnearr=6mv;im\nnearrow=6mv;il\nnedot=6s0,mw;rv\nnequiv=6si;sq\nnesear=84o;18n\nnesim=6rm,mw;qo\nnexist=6pw;n3\nnexists=6pw;n2\nnfr=2kob;1lf\nngE=6sn,mw;t4\nnge=6sx;u7\nngeq=6sx;u6\nngeqq=6sn,mw;t5\nngeqslant=8e6,mw;1f3\nnges=8e6,mw;1f4\nngsim=6t1;uh\nngt=6sv;u1\nngtr=6sv;u0\nnhArr=6oe;ln\nnharr=6ni;ju\nnhpar=8he;1j3\nni=6q3;nk\nnis=6ws;11u\nnisd=6wq;11s\nniv=6q3;nj\nnjcy=uy;cw\nnlArr=6od;ll\nnlE=6sm,mw;sy\nnlarr=6my;iu\nnldr=6cl;ea\nnle=6sw;u4\nnleftarrow=6my;it\nnleftrightarrow=6ni;jt\nnleq=6sw;u3\nnleqq=6sm,mw;sz\nnleqslant=8e5,mw;1ex\nnles=8e5,mw;1ey\nnless=6su;tx\nnlsim=6t0;uf\nnlt=6su;ty\nnltri=6wa;115\nnltrie=6wc;11b\nnmid=6qs;ou\nnopf=2kpr;1mo\nnot=4s;1u\nnotin=6q1;ng\nnotinE=6wp,mw;11q\nnotindot=6wl,mw;11m\nnotinva=6q1;nf\nnotinvb=6wn;11p\nnotinvc=6wm;11o\nnotni=6q4;nn\nnotniva=6q4;nm\nnotnivb=6wu;11w\nnotnivc=6wt;11v\nnpar=6qu;p4\nnparallel=6qu;p2\nnparsl=8hp,6hx;1j5\nnpart=6pu,mw;mw\nnpolint=8b8;1cu\nnpr=6tc;vd\nnprcue=6w0;10q\nnpre=8fj,mw;1gw\nnprec=6tc;vc\nnpreceq=8fj,mw;1gx\nnrArr=6of;lp\nnrarr=6mz;iw\nnrarrc=84z,mw;18s\nnrarrw=6n1,mw;ix\nnrightarrow=6mz;iv\nnrtri=6wb;118\nnrtrie=6wd;11e\nnsc=6td;vg\nnsccue=6w1;10s\nnsce=8fk,mw;1h2\nnscr=2klf;1k5\nnshortmid=6qs;os\nnshortparallel=6qu;p1\nnsim=6rl;qm\nnsime=6ro;qx\nnsimeq=6ro;qw\nnsmid=6qs;ot\nnspar=6qu;p3\nnsqsube=6w2;10u\nnsqsupe=6w3;10w\nnsub=6tg;vs\nnsubE=8g5,mw;1hv\nnsube=6tk;w2\nnsubset=6te,6he;vi\nnsubseteq=6tk;w1\nnsubseteqq=8g5,mw;1hw\nnsucc=6td;vf\nnsucceq=8fk,mw;1h3\nnsup=6th;vt\nnsupE=8g6,mw;1hz\nnsupe=6tl;w5\nnsupset=6tf,6he;vn\nnsupseteq=6tl;w4\nnsupseteqq=8g6,mw;1i0\nntgl=6t5;ur\nntilde=6p;42\nntlg=6t4;up\nntriangleleft=6wa;114\nntrianglelefteq=6wc;11a\nntriangleright=6wb;117\nntrianglerighteq=6wd;11d\nnu=ql;9f\nnum=z;5\nnumero=6ja;fy\nnumsp=6br;d5\nnvDash=6ul;y5\nnvHarr=83o;17u\nnvap=6rx,6he;ri\nnvdash=6uk;y4\nnvge=6sl,6he;su\nnvgt=1q,6he;q\nnvinfin=89q;1c5\nnvlArr=83m;17s\nnvle=6sk,6he;sr\nnvlt=1o,6he;l\nnvltrie=6us,6he;yf\nnvrArr=83n;17t\nnvrtrie=6ut,6he;yj\nnvsim=6rg,6he;q6\nnwArr=6om;ma\nnwarhk=84j;18g\nnwarr=6mu;ij\nnwarrow=6mu;ii\nnwnear=84n;18m\noS=79k;13h\noacute=6r;44\noast=6u3;xd\nocir=6u2;xb\nocirc=6s;45\nocy=u6;c5\nodash=6u5;xf\nodblac=9d;6l\nodiv=8c8;1dg\nodot=6u1;x9\nodsold=88s;1bn\noelig=9f;6n\nofcir=88v;1bp\nofr=2koc;1lg\nogon=kb;87\nograve=6q;43\nogt=88x;1br\nohbar=88l;1bi\nohm=q1;91\noint=6r2;pk\nolarr=6nu;k7\nolcir=88u;1bo\nolcross=88r;1bm\noline=6da;en\nolt=88w;1bq\nomacr=99;6j\nomega=qx;9u\nomicron=qn;9h\nomid=88m;1bj\nominus=6ty;x4\noopf=2kps;1mp\nopar=88n;1bk\noperp=88p;1bl\noplus=6tx;x2\nor=6qw;p8\norarr=6nv;k9\nord=8d9;1ea\norder=6k4;h1\norderof=6k4;h0\nordf=4q;1s\nordm=56;2h\norigof=6uu;yn\noror=8d2;1e4\norslope=8d3;1e5\norv=8d7;1e8\noscr=6k4;h2\noslash=6w;4a\nosol=6u0;x7\notilde=6t;46\notimes=6tz;x6\notimesas=8c6;1de\nouml=6u;47\novbar=6yl;12x\npar=6qt;oz\npara=52;2a\nparallel=6qt;ox\nparsim=8hf;1j4\nparsl=8hp;1j6\npart=6pu;my\npcy=u7;c6\npercnt=11;7\nperiod=1a;h\npermil=6cw;ed\nperp=6ud;xw\npertenk=6cx;ee\npfr=2kod;1lh\nphi=qu;9r\nphiv=r9;a2\nphmmat=6k3;gy\nphone=7im;162\npi=qo;9i\npitchfork=6vo;101\npiv=ra;a4\nplanck=6j3;fj\nplanckh=6j2;fh\nplankv=6j3;fk\nplus=17;f\nplusacir=8bn;1cz\nplusb=6u6;xh\npluscir=8bm;1cy\nplusdo=6qc;nz\nplusdu=8bp;1d1\npluse=8du;1el\nplusmn=4x;23\nplussim=8bq;1d2\nplustwo=8br;1d3\npm=4x;24\npointint=8b9;1cv\npopf=2kpt;1mq\npound=4j;1h\npr=6t6;uu\nprE=8fn;1h7\nprap=8fr;1he\nprcue=6t8;v0\npre=8fj;1h0\nprec=6t6;ut\nprecapprox=8fr;1hd\npreccurlyeq=6t8;uz\npreceq=8fj;1gz\nprecnapprox=8ft;1hh\nprecneqq=8fp;1h9\nprecnsim=6w8;10z\nprecsim=6ta;v5\nprime=6cy;ef\nprimes=6jd;g2\nprnE=8fp;1ha\nprnap=8ft;1hi\nprnsim=6w8;110\nprod=6q7;np\nprofalar=6y6;12v\nprofline=6xe;12e\nprofsurf=6xf;12f\nprop=6ql;oe\npropto=6ql;oc\nprsim=6ta;v6\nprurel=6uo;y8\npscr=2klh;1k6\npsi=qw;9t\npuncsp=6bs;d6\nqfr=2koe;1li\nqint=8b0;1co\nqopf=2kpu;1mr\nqprime=6dz;es\nqscr=2kli;1k7\nquaternions=6j1;ff\nquatint=8ba;1cw\nquest=1r;t\nquesteq=6sf;si\nquot=y;4\nrAarr=6or;mh\nrArr=6oi;lz\nrAtail=84c;18b\nrBarr=83z;181\nrHar=86c;19s\nrace=6rh,mp;qb\nracute=9h;6p\nradic=6qi;o8\nraemptyv=88j;1bg\nrang=7vt;172\nrangd=87m;1at\nrange=885;1b2\nrangle=7vt;171\nraquo=57;2i\nrarr=6mq;i6\nrarrap=86t;1ab\nrarrb=6p1;mm\nrarrbfs=84g;18f\nrarrc=84z;18t\nrarrfs=84e;18d\nrarrhk=6ne;jm\nrarrlp=6ng;jq\nrarrpl=85h;191\nrarrsim=86s;1aa\nrarrtl=6n7;j9\nrarrw=6n1;iz\nratail=84a;189\nratio=6ra;pz\nrationals=6je;g4\nrbarr=83x;17y\nrbbrk=7sj;16q\nrbrace=3h;1b\nrbrack=2l;y\nrbrke=87g;1an\nrbrksld=87i;1ap\nrbrkslu=87k;1ar\nrcaron=9l;6t\nrcedil=9j;6r\nrceil=6x5;124\nrcub=3h;1c\nrcy=u8;c7\nrdca=853;18w\nrdldhar=86h;19x\nrdquo=6cd;e2\nrdquor=6cd;e1\nrdsh=6nn;k0\nreal=6jg;g9\nrealine=6jf;g6\nrealpart=6jg;g8\nreals=6jh;gc\nrect=7fx;151\nreg=4u;1y\nrfisht=871;1ah\nrfloor=6x7;128\nrfr=2kof;1lj\nrhard=6o1;kr\nrharu=6o0;ko\nrharul=86k;1a0\nrho=qp;9j\nrhov=s1;ab\nrightarrow=6mq;i4\nrightarrowtail=6n7;j8\nrightharpoondown=6o1;kp\nrightharpoonup=6o0;km\nrightleftarrows=6o4;kz\nrightleftharpoons=6oc;lh\nrightrightarrows=6o9;la\nrightsquigarrow=6n1;iy\nrightthreetimes=6vg;zn\nring=ka;86\nrisingdotseq=6s3;s3\nrlarr=6o4;l0\nrlhar=6oc;lj\nrlm=6bz;dj\nrmoust=71t;133\nrmoustache=71t;132\nrnmid=8ha;1iz\nroang=7vx;176\nroarr=6pq;mq\nrobrk=7vr;16w\nropar=87a;1al\nropf=2kpv;1ms\nroplus=8by;1d7\nrotimes=8c5;1dd\nrpar=15;c\nrpargt=87o;1av\nrppolint=8b6;1cs\nrrarr=6o9;lb\nrsaquo=6d6;el\nrscr=2klj;1k8\nrsh=6nl;jy\nrsqb=2l;z\nrsquo=6c9;dv\nrsquor=6c9;du\nrthree=6vg;zo\nrtimes=6ve;zk\nrtri=7g9;15d\nrtrie=6ut;ym\nrtrif=7g8;15b\nrtriltri=89a;1by\nruluhar=86g;19w\nrx=6ji;ge\nsacute=9n;6v\nsbquo=6ca;dx\nsc=6t7;ux\nscE=8fo;1h8\nscap=8fs;1hg\nscaron=9t;71\nsccue=6t9;v3\nsce=8fk;1h6\nscedil=9r;6z\nscirc=9p;6x\nscnE=8fq;1hc\nscnap=8fu;1hk\nscnsim=6w9;112\nscpolint=8b7;1ct\nscsim=6tb;va\nscy=u9;c8\nsdot=6v9;zd\nsdotb=6u9;xn\nsdote=8di;1ec\nseArr=6oo;mc\nsearhk=84l;18j\nsearr=6mw;ip\nsearrow=6mw;io\nsect=4n;1l\nsemi=1n;k\nseswar=84p;18p\nsetminus=6qe;o2\nsetmn=6qe;o4\nsext=7qu;16n\nsfr=2kog;1lk\nsfrown=6xu;12q\nsharp=7lb;16h\nshchcy=uh;cg\nshcy=ug;cf\nshortmid=6qr;oo\nshortparallel=6qt;ow\nshy=4t;1v\nsigma=qr;9n\nsigmaf=qq;9l\nsigmav=qq;9m\nsim=6rg;qa\nsimdot=8dm;1ed\nsime=6rn;qu\nsimeq=6rn;qt\nsimg=8f2;1gb\nsimgE=8f4;1gd\nsiml=8f1;1ga\nsimlE=8f3;1gc\nsimne=6rq;r0\nsimplus=8bo;1d0\nsimrarr=86q;1a8\nslarr=6mo;hw\nsmallsetminus=6qe;o0\nsmashp=8c3;1db\nsmeparsl=89w;1c7\nsmid=6qr;op\nsmile=6xv;12t\nsmt=8fe;1go\nsmte=8fg;1gr\nsmtes=8fg,1e68;1gq\nsoftcy=uk;cj\nsol=1b;i\nsolb=890;1bu\nsolbar=6yn;12y\nsopf=2kpw;1mt\nspades=7kw;166\nspadesuit=7kw;165\nspar=6qt;oy\nsqcap=6tv;wx\nsqcaps=6tv,1e68;wv\nsqcup=6tw;x0\nsqcups=6tw,1e68;wy\nsqsub=6tr;wk\nsqsube=6tt;wr\nsqsubset=6tr;wj\nsqsubseteq=6tt;wq\nsqsup=6ts;wo\nsqsupe=6tu;wu\nsqsupset=6ts;wn\nsqsupseteq=6tu;wt\nsqu=7fl;14v\nsquare=7fl;14u\nsquarf=7fu;14y\nsquf=7fu;14z\nsrarr=6mq;i5\nsscr=2klk;1k9\nssetmn=6qe;o3\nssmile=6xv;12s\nsstarf=6va;ze\nstar=7ie;161\nstarf=7id;160\nstraightepsilon=s5;ac\nstraightphi=r9;a0\nstrns=4v;1z\nsub=6te;vl\nsubE=8g5;1hy\nsubdot=8fx;1hn\nsube=6ti;vw\nsubedot=8g3;1ht\nsubmult=8g1;1hr\nsubnE=8gb;1i8\nsubne=6tm;w9\nsubplus=8fz;1hp\nsubrarr=86x;1ae\nsubset=6te;vk\nsubseteq=6ti;vv\nsubseteqq=8g5;1hx\nsubsetneq=6tm;w8\nsubsetneqq=8gb;1i7\nsubsim=8g7;1i3\nsubsub=8gl;1ij\nsubsup=8gj;1ih\nsucc=6t7;uw\nsuccapprox=8fs;1hf\nsucccurlyeq=6t9;v2\nsucceq=8fk;1h5\nsuccnapprox=8fu;1hj\nsuccneqq=8fq;1hb\nsuccnsim=6w9;111\nsuccsim=6tb;v9\nsum=6q9;nt\nsung=7l6;16d\nsup=6tf;vr\nsup1=55;2g\nsup2=4y;25\nsup3=4z;26\nsupE=8g6;1i2\nsupdot=8fy;1ho\nsupdsub=8go;1im\nsupe=6tj;vz\nsupedot=8g4;1hu\nsuphsol=7ux;16s\nsuphsub=8gn;1il\nsuplarr=86z;1af\nsupmult=8g2;1hs\nsupnE=8gc;1ic\nsupne=6tn;wd\nsupplus=8g0;1hq\nsupset=6tf;vq\nsupseteq=6tj;vy\nsupseteqq=8g6;1i1\nsupsetneq=6tn;wc\nsupsetneqq=8gc;1ib\nsupsim=8g8;1i4\nsupsub=8gk;1ii\nsupsup=8gm;1ik\nswArr=6op;md\nswarhk=84m;18l\nswarr=6mx;is\nswarrow=6mx;ir\nswnwar=84q;18r\nszlig=67;3k\ntarget=6xi;12h\ntau=qs;9o\ntbrk=71w;135\ntcaron=9x;75\ntcedil=9v;73\ntcy=ua;c9\ntdot=6hn;f4\ntelrec=6xh;12g\ntfr=2koh;1ll\nthere4=6r8;pv\ntherefore=6r8;pu\ntheta=qg;9a\nthetasym=r5;9v\nthetav=r5;9x\nthickapprox=6rs;r3\nthicksim=6rg;q7\nthinsp=6bt;d8\nthkap=6rs;r7\nthksim=6rg;q8\nthorn=72;4g\ntilde=kc;89\ntimes=5z;3c\ntimesb=6u8;xl\ntimesbar=8c1;1da\ntimesd=8c0;1d9\ntint=6r1;ph\ntoea=84o;18o\ntop=6uc;xt\ntopbot=6ye;12w\ntopcir=8hd;1j2\ntopf=2kpx;1mu\ntopfork=8gq;1io\ntosa=84p;18q\ntprime=6d0;eh\ntrade=6jm;gg\ntriangle=7g5;158\ntriangledown=7gf;15i\ntriangleleft=7gj;15m\ntrianglelefteq=6us;yh\ntriangleq=6sc;sg\ntriangleright=7g9;15c\ntrianglerighteq=6ut;yl\ntridot=7ho;15r\ntrie=6sc;sh\ntriminus=8ca;1di\ntriplus=8c9;1dh\ntrisb=899;1bx\ntritime=8cb;1dj\ntrpezium=736;13d\ntscr=2kll;1ka\ntscy=ue;cd\ntshcy=uz;cx\ntstrok=9z;77\ntwixt=6ss;tu\ntwoheadleftarrow=6n2;j0\ntwoheadrightarrow=6n4;j3\nuArr=6oh;lv\nuHar=86b;19r\nuacute=6y;4c\nuarr=6mp;i1\nubrcy=v2;cz\nubreve=a5;7d\nucirc=6z;4d\nucy=ub;ca\nudarr=6o5;l2\nudblac=a9;7h\nudhar=86m;1a3\nufisht=872;1ai\nufr=2koi;1lm\nugrave=6x;4b\nuharl=6nz;kl\nuharr=6ny;ki\nuhblk=7eo;14n\nulcorn=6xo;12j\nulcorner=6xo;12i\nulcrop=6xb;12c\nultri=7i0;15u\numacr=a3;7b\numl=4o;1p\nuogon=ab;7j\nuopf=2kpy;1mv\nuparrow=6mp;i0\nupdownarrow=6mt;if\nupharpoonleft=6nz;kj\nupharpoonright=6ny;kg\nuplus=6tq;wg\nupsi=qt;9q\nupsih=r6;9y\nupsilon=qt;9p\nupuparrows=6o8;l8\nurcorn=6xp;12l\nurcorner=6xp;12k\nurcrop=6xa;12b\nuring=a7;7f\nurtri=7i1;15v\nuscr=2klm;1kb\nutdot=6wg;11h\nutilde=a1;79\nutri=7g5;159\nutrif=7g4;157\nuuarr=6o8;l9\nuuml=70;4e\nuwangle=887;1b4\nvArr=6ol;m9\nvBar=8h4;1iu\nvBarv=8h5;1iv\nvDash=6ug;y0\nvangrt=87w;1az\nvarepsilon=s5;ad\nvarkappa=s0;a8\nvarnothing=6px;n4\nvarphi=r9;a1\nvarpi=ra;a3\nvarpropto=6ql;ob\nvarr=6mt;ig\nvarrho=s1;aa\nvarsigma=qq;9k\nvarsubsetneq=6tm,1e68;w6\nvarsubsetneqq=8gb,1e68;1i5\nvarsupsetneq=6tn,1e68;wa\nvarsupsetneqq=8gc,1e68;1i9\nvartheta=r5;9w\nvartriangleleft=6uq;y9\nvartriangleright=6ur;yc\nvcy=tu;bt\nvdash=6ua;xp\nvee=6qw;p7\nveebar=6uz;yu\nveeeq=6sa;sf\nvellip=6we;11f\nverbar=3g;19\nvert=3g;1a\nvfr=2koj;1ln\nvltri=6uq;yb\nvnsub=6te,6he;vj\nvnsup=6tf,6he;vo\nvopf=2kpz;1mw\nvprop=6ql;od\nvrtri=6ur;ye\nvscr=2kln;1kc\nvsubnE=8gb,1e68;1i6\nvsubne=6tm,1e68;w7\nvsupnE=8gc,1e68;1ia\nvsupne=6tn,1e68;wb\nvzigzag=87u;1ay\nwcirc=ad;7l\nwedbar=8db;1eb\nwedge=6qv;p5\nwedgeq=6s9;se\nweierp=6jc;g0\nwfr=2kok;1lo\nwopf=2kq0;1mx\nwp=6jc;g1\nwr=6rk;qk\nwreath=6rk;qj\nwscr=2klo;1kd\nxcap=6v6;z6\nxcirc=7hr;15t\nxcup=6v7;z9\nxdtri=7gd;15f\nxfr=2kol;1lp\nxhArr=7wa;17o\nxharr=7w7;17f\nxi=qm;9g\nxlArr=7w8;17i\nxlarr=7w5;179\nxmap=7wc;17q\nxnis=6wr;11t\nxodot=8ao;1ce\nxopf=2kq1;1my\nxoplus=8ap;1cg\nxotime=8aq;1ci\nxrArr=7w9;17l\nxrarr=7w6;17c\nxscr=2klp;1ke\nxsqcup=8au;1cm\nxuplus=8as;1ck\nxutri=7g3;155\nxvee=6v5;z2\nxwedge=6v4;yz\nyacute=71;4f\nyacy=un;cm\nycirc=af;7n\nycy=uj;ci\nyen=4l;1j\nyfr=2kom;1lq\nyicy=uv;ct\nyopf=2kq2;1mz\nyscr=2klq;1kf\nyucy=um;cl\nyuml=73;4h\nzacute=ai;7q\nzcaron=am;7u\nzcy=tz;by\nzdot=ak;7s\nzeetrf=6js;gk\nzeta=qe;98\nzfr=2kon;1lr\nzhcy=ty;bx\nzigrarr=6ot;mi\nzopf=2kq3;1n0\nzscr=2klr;1kg\nzwj=6bx;dh\nzwnj=6bw;dg" + +} + +final class MutexLock: NSLocking { + + private let locker: NSLocking + + init() { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) + if #available(iOS 10.0, macOS 10.12, watchOS 3.0, tvOS 10.0, *) { + locker = UnfairLock() + } else { + locker = Mutex() + } + #else + locker = Mutex() + #endif + } + + func lock() { + locker.lock() + } + + func unlock() { + locker.unlock() + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Evaluator.swift b/Swiftgram/SwiftSoup/Sources/Evaluator.swift new file mode 100644 index 00000000000..0ecf21535ef --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Evaluator.swift @@ -0,0 +1,720 @@ +// +// Evaluator.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 22/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * Evaluates that an element matches the selector. + */ +open class Evaluator { + public init () {} + + /** + * Test if the element meets the evaluator's requirements. + * + * @param root Root of the matching subtree + * @param element tested element + * @return Returns true if the requirements are met or + * false otherwise + */ + open func matches(_ root: Element, _ element: Element)throws->Bool { + preconditionFailure("self method must be overridden") + } + + open func toString() -> String { + preconditionFailure("self method must be overridden") + } + + /** + * Evaluator for tag name + */ + public class Tag: Evaluator { + private let tagName: String + private let tagNameNormal: String + + public init(_ tagName: String) { + self.tagName = tagName + self.tagNameNormal = tagName.lowercased() + } + + open override func matches(_ root: Element, _ element: Element)throws->Bool { + return element.tagNameNormal() == tagNameNormal + } + + open override func toString() -> String { + return String(tagName) + } + } + + /** + * Evaluator for tag name that ends with + */ + public final class TagEndsWith: Evaluator { + private let tagName: String + + public init(_ tagName: String) { + self.tagName = tagName + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return (element.tagName().hasSuffix(tagName)) + } + + public override func toString() -> String { + return String(tagName) + } + } + + /** + * Evaluator for element id + */ + public final class Id: Evaluator { + private let id: String + + public init(_ id: String) { + self.id = id + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return (id == element.id()) + } + + public override func toString() -> String { + return "#\(id)" + } + + } + + /** + * Evaluator for element class + */ + public final class Class: Evaluator { + private let className: String + + public init(_ className: String) { + self.className = className + } + + public override func matches(_ root: Element, _ element: Element) -> Bool { + return (element.hasClass(className)) + } + + public override func toString() -> String { + return ".\(className)" + } + + } + + /** + * Evaluator for attribute name matching + */ + public final class Attribute: Evaluator { + private let key: String + + public init(_ key: String) { + self.key = key + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return element.hasAttr(key) + } + + public override func toString() -> String { + return "[\(key)]" + } + + } + + /** + * Evaluator for attribute name prefix matching + */ + public final class AttributeStarting: Evaluator { + private let keyPrefix: String + + public init(_ keyPrefix: String)throws { + try Validate.notEmpty(string: keyPrefix) + self.keyPrefix = keyPrefix.lowercased() + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if let values = element.getAttributes() { + for attribute in values where attribute.getKey().lowercased().hasPrefix(keyPrefix) { + return true + } + } + return false + } + + public override func toString() -> String { + return "[^\(keyPrefix)]" + } + + } + + /** + * Evaluator for attribute name/value matching + */ + public final class AttributeWithValue: AttributeKeyPair { + public override init(_ key: String, _ value: String)throws { + try super.init(key, value) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if element.hasAttr(key) { + let string = try element.attr(key) + return value.equalsIgnoreCase(string: string.trim()) + } + return false + } + + public override func toString() -> String { + return "[\(key)=\(value)]" + } + + } + + /** + * Evaluator for attribute name != value matching + */ + public final class AttributeWithValueNot: AttributeKeyPair { + public override init(_ key: String, _ value: String)throws { + try super.init(key, value) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let string = try element.attr(key) + return !value.equalsIgnoreCase(string: string) + } + + public override func toString() -> String { + return "[\(key)!=\(value)]" + } + + } + + /** + * Evaluator for attribute name/value matching (value prefix) + */ + public final class AttributeWithValueStarting: AttributeKeyPair { + public override init(_ key: String, _ value: String)throws { + try super.init(key, value) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if element.hasAttr(key) { + return try element.attr(key).lowercased().hasPrefix(value) // value is lower case already + } + return false + } + + public override func toString() -> String { + return "[\(key)^=\(value)]" + } + + } + + /** + * Evaluator for attribute name/value matching (value ending) + */ + public final class AttributeWithValueEnding: AttributeKeyPair { + public override init(_ key: String, _ value: String)throws { + try super.init(key, value) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if element.hasAttr(key) { + return try element.attr(key).lowercased().hasSuffix(value) // value is lower case + } + return false + } + + public override func toString() -> String { + return "[\(key)$=\(value)]" + } + + } + + /** + * Evaluator for attribute name/value matching (value containing) + */ + public final class AttributeWithValueContaining: AttributeKeyPair { + public override init(_ key: String, _ value: String)throws { + try super.init(key, value) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if element.hasAttr(key) { + return try element.attr(key).lowercased().contains(value) // value is lower case + } + return false + } + + public override func toString() -> String { + return "[\(key)*=\(value)]" + } + + } + + /** + * Evaluator for attribute name/value matching (value regex matching) + */ + public final class AttributeWithValueMatching: Evaluator { + let key: String + let pattern: Pattern + + public init(_ key: String, _ pattern: Pattern) { + self.key = key.trim().lowercased() + self.pattern = pattern + super.init() + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + if element.hasAttr(key) { + let string = try element.attr(key) + return pattern.matcher(in: string).find() + } + return false + } + + public override func toString() -> String { + return "[\(key)~=\(pattern.toString())]" + } + + } + + /** + * Abstract evaluator for attribute name/value matching + */ + public class AttributeKeyPair: Evaluator { + let key: String + var value: String + + public init(_ key: String, _ value2: String)throws { + var value2 = value2 + try Validate.notEmpty(string: key) + try Validate.notEmpty(string: value2) + + self.key = key.trim().lowercased() + if value2.startsWith("\"") && value2.hasSuffix("\"") || value2.startsWith("'") && value2.hasSuffix("'") { + value2 = value2.substring(1, value2.count-2) + } + self.value = value2.trim().lowercased() + } + + open override func matches(_ root: Element, _ element: Element)throws->Bool { + preconditionFailure("self method must be overridden") + } + } + + /** + * Evaluator for any / all element matching + */ + public final class AllElements: Evaluator { + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return true + } + + public override func toString() -> String { + return "*" + } + } + + /** + * Evaluator for matching by sibling index number (e {@literal <} idx) + */ + public final class IndexLessThan: IndexEvaluator { + public override init(_ index: Int) { + super.init(index) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return try element.elementSiblingIndex() < index + } + + public override func toString() -> String { + return ":lt(\(index))" + } + + } + + /** + * Evaluator for matching by sibling index number (e {@literal >} idx) + */ + public final class IndexGreaterThan: IndexEvaluator { + public override init(_ index: Int) { + super.init(index) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return try element.elementSiblingIndex() > index + } + + public override func toString() -> String { + return ":gt(\(index))" + } + + } + + /** + * Evaluator for matching by sibling index number (e = idx) + */ + public final class IndexEquals: IndexEvaluator { + public override init(_ index: Int) { + super.init(index) + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return try element.elementSiblingIndex() == index + } + + public override func toString() -> String { + return ":eq(\(index))" + } + + } + + /** + * Evaluator for matching the last sibling (css :last-child) + */ + public final class IsLastChild: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + + if let parent = element.parent() { + let index = try element.elementSiblingIndex() + return !(parent is Document) && index == (parent.getChildNodes().count - 1) + } + return false + } + + public override func toString() -> String { + return ":last-child" + } + } + + public final class IsFirstOfType: IsNthOfType { + public init() { + super.init(0, 1) + } + public override func toString() -> String { + return ":first-of-type" + } + } + + public final class IsLastOfType: IsNthLastOfType { + public init() { + super.init(0, 1) + } + public override func toString() -> String { + return ":last-of-type" + } + } + + public class CssNthEvaluator: Evaluator { + public let a: Int + public let b: Int + + public init(_ a: Int, _ b: Int) { + self.a = a + self.b = b + } + public init(_ b: Int) { + self.a = 0 + self.b = b + } + + open override func matches(_ root: Element, _ element: Element)throws->Bool { + let p: Element? = element.parent() + if (p == nil || (((p as? Document) != nil))) {return false} + + let pos: Int = try calculatePosition(root, element) + if (a == 0) {return pos == b} + + return (pos-b)*a >= 0 && (pos-b)%a==0 + } + + open override func toString() -> String { + if (a == 0) { + return ":\(getPseudoClass())(\(b))" + } + if (b == 0) { + return ":\(getPseudoClass())(\(a))" + } + return ":\(getPseudoClass())(\(a)\(b))" + } + + open func getPseudoClass() -> String { + preconditionFailure("self method must be overridden") + } + open func calculatePosition(_ root: Element, _ element: Element)throws->Int { + preconditionFailure("self method must be overridden") + } + } + + /** + * css-compatible Evaluator for :eq (css :nth-child) + * + * @see IndexEquals + */ + public final class IsNthChild: CssNthEvaluator { + + public override init(_ a: Int, _ b: Int) { + super.init(a, b) + } + + public override func calculatePosition(_ root: Element, _ element: Element)throws->Int { + return try element.elementSiblingIndex()+1 + } + + public override func getPseudoClass() -> String { + return "nth-child" + } + } + + /** + * css pseudo class :nth-last-child) + * + * @see IndexEquals + */ + public final class IsNthLastChild: CssNthEvaluator { + public override init(_ a: Int, _ b: Int) { + super.init(a, b) + } + + public override func calculatePosition(_ root: Element, _ element: Element)throws->Int { + var i = 0 + + if let l = element.parent() { + i = l.children().array().count + } + return i - (try element.elementSiblingIndex()) + } + + public override func getPseudoClass() -> String { + return "nth-last-child" + } + } + + /** + * css pseudo class nth-of-type + * + */ + public class IsNthOfType: CssNthEvaluator { + public override init(_ a: Int, _ b: Int) { + super.init(a, b) + } + + open override func calculatePosition(_ root: Element, _ element: Element) -> Int { + var pos = 0 + let family: Elements? = element.parent()?.children() + if let array = family?.array() { + for el in array { + if (el.tag() == element.tag()) {pos+=1} + if (el === element) {break} + } + } + + return pos + } + + open override func getPseudoClass() -> String { + return "nth-of-type" + } + } + + public class IsNthLastOfType: CssNthEvaluator { + + public override init(_ a: Int, _ b: Int) { + super.init(a, b) + } + + open override func calculatePosition(_ root: Element, _ element: Element)throws->Int { + var pos = 0 + if let family = element.parent()?.children() { + let x = try element.elementSiblingIndex() + for i in x.. String { + return "nth-last-of-type" + } + } + + /** + * Evaluator for matching the first sibling (css :first-child) + */ + public final class IsFirstChild: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let p = element.parent() + if(p != nil && !(((p as? Document) != nil))) { + return (try element.elementSiblingIndex()) == 0 + } + return false + } + + public override func toString() -> String { + return ":first-child" + } + } + + /** + * css3 pseudo-class :root + * @see :root selector + * + */ + public final class IsRoot: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let r: Element = ((root as? Document) != nil) ? root.child(0) : root + return element === r + } + public override func toString() -> String { + return ":root" + } + } + + public final class IsOnlyChild: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let p = element.parent() + return p != nil && !((p as? Document) != nil) && element.siblingElements().array().count == 0 + } + public override func toString() -> String { + return ":only-child" + } + } + + public final class IsOnlyOfType: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let p = element.parent() + if (p == nil || (p as? Document) != nil) {return false} + + var pos = 0 + if let family = p?.children().array() { + for el in family { + if (el.tag() == element.tag()) {pos+=1} + } + } + return pos == 1 + } + + public override func toString() -> String { + return ":only-of-type" + } + } + + public final class IsEmpty: Evaluator { + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let family: Array = element.getChildNodes() + for n in family { + if (!((n as? Comment) != nil || (n as? XmlDeclaration) != nil || (n as? DocumentType) != nil)) {return false} + } + return true + } + + public override func toString() -> String { + return ":empty" + } + } + + /** + * Abstract evaluator for sibling index matching + * + * @author ant + */ + public class IndexEvaluator: Evaluator { + let index: Int + + public init(_ index: Int) { + self.index = index + } + } + + /** + * Evaluator for matching Element (and its descendants) text + */ + public final class ContainsText: Evaluator { + private let searchText: String + + public init(_ searchText: String) { + self.searchText = searchText.lowercased() + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return (try element.text().lowercased().contains(searchText)) + } + + public override func toString() -> String { + return ":contains(\(searchText)" + } + } + + /** + * Evaluator for matching Element's own text + */ + public final class ContainsOwnText: Evaluator { + private let searchText: String + + public init(_ searchText: String) { + self.searchText = searchText.lowercased() + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + return (element.ownText().lowercased().contains(searchText)) + } + + public override func toString() -> String { + return ":containsOwn(\(searchText)" + } + } + + /** + * Evaluator for matching Element (and its descendants) text with regex + */ + public final class Matches: Evaluator { + private let pattern: Pattern + + public init(_ pattern: Pattern) { + self.pattern = pattern + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let m = try pattern.matcher(in: element.text()) + return m.find() + } + + public override func toString() -> String { + return ":matches(\(pattern)" + } + } + + /** + * Evaluator for matching Element's own text with regex + */ + public final class MatchesOwn: Evaluator { + private let pattern: Pattern + + public init(_ pattern: Pattern) { + self.pattern = pattern + } + + public override func matches(_ root: Element, _ element: Element)throws->Bool { + let m = pattern.matcher(in: element.ownText()) + return m.find() + } + + public override func toString() -> String { + return ":matchesOwn(\(pattern.toString())" + } + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Exception.swift b/Swiftgram/SwiftSoup/Sources/Exception.swift new file mode 100644 index 00000000000..a4ab97ab94d --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Exception.swift @@ -0,0 +1,22 @@ +// +// Exception.swift +// SwifSoup +// +// Created by Nabil Chatbi on 02/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +public enum ExceptionType { + case IllegalArgumentException + case IOException + case XmlDeclaration + case MalformedURLException + case CloneNotSupportedException + case SelectorParseException +} + +public enum Exception: Error { + case Error(type:ExceptionType, Message: String) +} diff --git a/Swiftgram/SwiftSoup/Sources/FormElement.swift b/Swiftgram/SwiftSoup/Sources/FormElement.swift new file mode 100644 index 00000000000..a15754fa04b --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/FormElement.swift @@ -0,0 +1,125 @@ +// +// FormElement.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * A HTML Form Element provides ready access to the form fields/controls that are associated with it. It also allows a + * form to easily be submitted. + */ +public class FormElement: Element { + private let _elements: Elements = Elements() + + /** + * Create a new, standalone form element. + * + * @param tag tag of this element + * @param baseUri the base URI + * @param attributes initial attributes + */ + public override init(_ tag: Tag, _ baseUri: String, _ attributes: Attributes) { + super.init(tag, baseUri, attributes) + } + + /** + * Get the list of form control elements associated with this form. + * @return form controls associated with this element. + */ + public func elements() -> Elements { + return _elements + } + + /** + * Add a form control element to this form. + * @param element form control to add + * @return this form element, for chaining + */ + @discardableResult + public func addElement(_ element: Element) -> FormElement { + _elements.add(element) + return self + } + + //todo: + /** + * Prepare to submit this form. A Connection object is created with the request set up from the form values. You + * can then set up other options (like user-agent, timeout, cookies), then execute it. + * @return a connection prepared from the values of this form. + * @throws IllegalArgumentException if the form's absolute action URL cannot be determined. Make sure you pass the + * document's base URI when parsing. + */ +// public func submit()throws->Connection { +// let action: String = hasAttr("action") ? try absUrl("action") : try baseUri() +// Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.") +// Connection.Method method = attr("method").toUpperCase().equals("POST") ? +// Connection.Method.POST : Connection.Method.GET +// +// return Jsoup.connect(action) +// .data(formData()) +// .method(method) +// } + + //todo: + /** + * Get the data that this form submits. The returned list is a copy of the data, and changes to the contents of the + * list will not be reflected in the DOM. + * @return a list of key vals + */ +// public List formData() { +// ArrayList data = new ArrayList(); +// +// // iterate the form control elements and accumulate their values +// for (Element el: elements) { +// if (!el.tag().isFormSubmittable()) continue; // contents are form listable, superset of submitable +// if (el.hasAttr("disabled")) continue; // skip disabled form inputs +// String name = el.attr("name"); +// if (name.length() == 0) continue; +// String type = el.attr("type"); +// +// if ("select".equals(el.tagName())) { +// Elements options = el.select("option[selected]"); +// boolean set = false; +// for (Element option: options) { +// data.add(HttpConnection.KeyVal.create(name, option.val())); +// set = true; +// } +// if (!set) { +// Element option = el.select("option").first(); +// if (option != null) +// data.add(HttpConnection.KeyVal.create(name, option.val())); +// } +// } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) { +// // only add checkbox or radio if they have the checked attribute +// if (el.hasAttr("checked")) { +// final String val = el.val().length() > 0 ? el.val() : "on"; +// data.add(HttpConnection.KeyVal.create(name, val)); +// } +// } else { +// data.add(HttpConnection.KeyVal.create(name, el.val())); +// } +// } +// return data; +// } + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = FormElement(_tag, baseUri!, attributes!) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = FormElement(_tag, baseUri!, attributes!) + return copy(clone: clone, parent: parent) + } + public override func copy(clone: Node, parent: Node?) -> Node { + let clone = clone as! FormElement + for att in _elements.array() { + clone._elements.add(att) + } + return super.copy(clone: clone, parent: parent) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/HtmlTreeBuilder.swift b/Swiftgram/SwiftSoup/Sources/HtmlTreeBuilder.swift new file mode 100644 index 00000000000..4f0fb9ec60f --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/HtmlTreeBuilder.swift @@ -0,0 +1,781 @@ +// +// HtmlTreeBuilder.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 24/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * HTML Tree Builder; creates a DOM from Tokens. + */ +class HtmlTreeBuilder: TreeBuilder { + + private enum TagSets { + // tag searches + static let inScope = ["applet", "caption", "html", "table", "td", "th", "marquee", "object"] + static let list = ["ol", "ul"] + static let button = ["button"] + static let tableScope = ["html", "table"] + static let selectScope = ["optgroup", "option"] + static let endTags = ["dd", "dt", "li", "option", "optgroup", "p", "rp", "rt"] + static let titleTextarea = ["title", "textarea"] + static let frames = ["iframe", "noembed", "noframes", "style", "xmp"] + + static let special: Set = ["address", "applet", "area", "article", "aside", "base", "basefont", "bgsound", + "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "command", "dd", + "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form", + "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", + "iframe", "img", "input", "isindex", "li", "link", "listing", "marquee", "menu", "meta", "nav", + "noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script", + "section", "select", "style", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", + "title", "tr", "ul", "wbr", "xmp"] + } + + private var _state: HtmlTreeBuilderState = HtmlTreeBuilderState.Initial // the current state + private var _originalState: HtmlTreeBuilderState = HtmlTreeBuilderState.Initial // original / marked state + + private var baseUriSetFromDoc: Bool = false + private var headElement: Element? // the current head element + private var formElement: FormElement? // the current form element + private var contextElement: Element? // fragment parse context -- could be null even if fragment parsing + private var formattingElements: Array = Array() // active (open) formatting elements + private var pendingTableCharacters: Array = Array() // chars in table to be shifted out + private var emptyEnd: Token.EndTag = Token.EndTag() // reused empty end tag + + private var _framesetOk: Bool = true // if ok to go into frameset + private var fosterInserts: Bool = false // if next inserts should be fostered + private var fragmentParsing: Bool = false // if parsing a fragment of html + + public override init() { + super.init() + } + + public override func defaultSettings() -> ParseSettings { + return ParseSettings.htmlDefault + } + + override func parse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Document { + _state = HtmlTreeBuilderState.Initial + baseUriSetFromDoc = false + return try super.parse(input, baseUri, errors, settings) + } + + func parseFragment(_ inputFragment: String, _ context: Element?, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Array { + // context may be null + _state = HtmlTreeBuilderState.Initial + initialiseParse(inputFragment, baseUri, errors, settings) + contextElement = context + fragmentParsing = true + var root: Element? = nil + + if let context = context { + if let d = context.ownerDocument() { // quirks setup: + doc.quirksMode(d.quirksMode()) + } + + // initialise the tokeniser state: + switch context.tagName() { + case TagSets.titleTextarea: + tokeniser.transition(TokeniserState.Rcdata) + case TagSets.frames: + tokeniser.transition(TokeniserState.Rawtext) + case "script": + tokeniser.transition(TokeniserState.ScriptData) + case "noscript": + tokeniser.transition(TokeniserState.Data) // if scripting enabled, rawtext + case "plaintext": + tokeniser.transition(TokeniserState.Data) + default: + tokeniser.transition(TokeniserState.Data) + } + + root = try Element(Tag.valueOf("html", settings), baseUri) + try Validate.notNull(obj: root) + try doc.appendChild(root!) + stack.append(root!) + resetInsertionMode() + + // setup form element to nearest form on context (up ancestor chain). ensures form controls are associated + // with form correctly + let contextChain: Elements = context.parents() + contextChain.add(0, context) + for parent: Element in contextChain.array() { + if let x = (parent as? FormElement) { + formElement = x + break + } + } + } + + try runParser() + if (context != nil && root != nil) { + return root!.getChildNodes() + } else { + return doc.getChildNodes() + } + } + + @discardableResult + public override func process(_ token: Token)throws->Bool { + currentToken = token + return try self._state.process(token, self) + } + + @discardableResult + func process(_ token: Token, _ state: HtmlTreeBuilderState)throws->Bool { + currentToken = token + return try state.process(token, self) + } + + func transition(_ state: HtmlTreeBuilderState) { + self._state = state + } + + func state() -> HtmlTreeBuilderState { + return _state + } + + func markInsertionMode() { + _originalState = _state + } + + func originalState() -> HtmlTreeBuilderState { + return _originalState + } + + func framesetOk(_ framesetOk: Bool) { + self._framesetOk = framesetOk + } + + func framesetOk() -> Bool { + return _framesetOk + } + + func getDocument() -> Document { + return doc + } + + func getBaseUri() -> String { + return baseUri + } + + func maybeSetBaseUri(_ base: Element)throws { + if (baseUriSetFromDoc) { // only listen to the first in parse + return + } + + let href: String = try base.absUrl("href") + if (href.count != 0) { // ignore etc + baseUri = href + baseUriSetFromDoc = true + try doc.setBaseUri(href) // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants + } + } + + func isFragmentParsing() -> Bool { + return fragmentParsing + } + + func error(_ state: HtmlTreeBuilderState) { + if (errors.canAddError() && currentToken != nil) { + errors.add(ParseError(reader.getPos(), "Unexpected token [\(currentToken!.tokenType())] when in state [\(state.rawValue)]")) + } + } + + @discardableResult + func insert(_ startTag: Token.StartTag)throws->Element { + // handle empty unknown tags + // when the spec expects an empty tag, will directly hit insertEmpty, so won't generate this fake end tag. + if (startTag.isSelfClosing()) { + let el: Element = try insertEmpty(startTag) + stack.append(el) + tokeniser.transition(TokeniserState.Data) // handles + + var tagPending: Token.Tag = Token.Tag() // tag we are building up + let startPending: Token.StartTag = Token.StartTag() + let endPending: Token.EndTag = Token.EndTag() + let charPending: Token.Char = Token.Char() + let doctypePending: Token.Doctype = Token.Doctype() // doctype building up + let commentPending: Token.Comment = Token.Comment() // comment building up + private var lastStartTag: String? // the last start tag emitted, to test appropriate end tag + private var selfClosingFlagAcknowledged: Bool = true + + init(_ reader: CharacterReader, _ errors: ParseErrorList?) { + self.reader = reader + self.errors = errors + } + + func read()throws->Token { + if (!selfClosingFlagAcknowledged) { + error("Self closing flag not acknowledged") + selfClosingFlagAcknowledged = true + } + + while (!isEmitPending) { + try state.read(self, reader) + } + + // if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read: + if !charsBuilder.isEmpty { + let str: String = charsBuilder.toString() + charsBuilder.clear() + charsString = nil + return charPending.data(str) + } else if (charsString != nil) { + let token: Token = charPending.data(charsString!) + charsString = nil + return token + } else { + isEmitPending = false + return emitPending! + } + } + + func emit(_ token: Token)throws { + try Validate.isFalse(val: isEmitPending, msg: "There is an unread token pending!") + + emitPending = token + isEmitPending = true + + if (token.type == Token.TokenType.StartTag) { + let startTag: Token.StartTag = token as! Token.StartTag + lastStartTag = startTag._tagName! + if (startTag._selfClosing) { + selfClosingFlagAcknowledged = false + } + } else if (token.type == Token.TokenType.EndTag) { + let endTag: Token.EndTag = token as! Token.EndTag + if (endTag._attributes.size() != 0) { + error("Attributes incorrectly present on end tag") + } + } + } + + func emit(_ str: String ) { + // buffer strings up until last string token found, to emit only one token for a run of character refs etc. + // does not set isEmitPending; read checks that + if (charsString == nil) { + charsString = str + } else { + if charsBuilder.isEmpty { // switching to string builder as more than one emit before read + charsBuilder.append(charsString!) + } + charsBuilder.append(str) + } + } + + func emit(_ chars: [UnicodeScalar]) { + emit(String(chars.map {Character($0)})) + } + + // func emit(_ codepoints: [Int]) { + // emit(String(codepoints, 0, codepoints.length)); + // } + + func emit(_ c: UnicodeScalar) { + emit(String(c)) + } + + func getState() -> TokeniserState { + return state + } + + func transition(_ state: TokeniserState) { + self.state = state + } + + func advanceTransition(_ state: TokeniserState) { + reader.advance() + self.state = state + } + + func acknowledgeSelfClosingFlag() { + selfClosingFlagAcknowledged = true + } + + func consumeCharacterReference(_ additionalAllowedCharacter: UnicodeScalar?, _ inAttribute: Bool)throws->[UnicodeScalar]? { + if (reader.isEmpty()) { + return nil + } + if (additionalAllowedCharacter != nil && additionalAllowedCharacter == reader.current()) { + return nil + } + if (reader.matchesAnySorted(Tokeniser.notCharRefCharsSorted)) { + return nil + } + + reader.markPos() + if (reader.matchConsume("#")) { // numbered + let isHexMode: Bool = reader.matchConsumeIgnoreCase("X") + let numRef: String = isHexMode ? reader.consumeHexSequence() : reader.consumeDigitSequence() + if (numRef.unicodeScalars.count == 0) { // didn't match anything + characterReferenceError("numeric reference with no numerals") + reader.rewindToMark() + return nil + } + if (!reader.matchConsume(";")) { + characterReferenceError("missing semicolon") // missing semi + } + var charval: Int = -1 + + let base: Int = isHexMode ? 16 : 10 + if let num = Int(numRef, radix: base) { + charval = num + } + + if (charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF) { + characterReferenceError("character outside of valid range") + return [Tokeniser.replacementChar] + } else { + // todo: implement number replacement table + // todo: check for extra illegal unicode points as parse errors + return [UnicodeScalar(charval)!] + } + } else { // named + // get as many letters as possible, and look for matching entities. + let nameRef: String = reader.consumeLetterThenDigitSequence() + let looksLegit: Bool = reader.matches(";") + // found if a base named entity without a ;, or an extended entity with the ;. + let found: Bool = (Entities.isBaseNamedEntity(nameRef) || (Entities.isNamedEntity(nameRef) && looksLegit)) + + if (!found) { + reader.rewindToMark() + if (looksLegit) { // named with semicolon + characterReferenceError("invalid named referenece '\(nameRef)'") + } + return nil + } + if (inAttribute && (reader.matchesLetter() || reader.matchesDigit() || reader.matchesAny("=", "-", "_"))) { + // don't want that to match + reader.rewindToMark() + return nil + } + if (!reader.matchConsume(";")) { + characterReferenceError("missing semicolon") // missing semi + } + if let points = Entities.codepointsForName(nameRef) { + if points.count > 2 { + try Validate.fail(msg: "Unexpected characters returned for \(nameRef) num: \(points.count)") + } + return points + } + try Validate.fail(msg: "Entity name not found: \(nameRef)") + return [] + } + } + + @discardableResult + func createTagPending(_ start: Bool)->Token.Tag { + tagPending = start ? startPending.reset() : endPending.reset() + return tagPending + } + + func emitTagPending()throws { + try tagPending.finaliseTag() + try emit(tagPending) + } + + func createCommentPending() { + commentPending.reset() + } + + func emitCommentPending()throws { + try emit(commentPending) + } + + func createDoctypePending() { + doctypePending.reset() + } + + func emitDoctypePending()throws { + try emit(doctypePending) + } + + func createTempBuffer() { + Token.reset(dataBuffer) + } + + func isAppropriateEndTagToken()throws->Bool { + if(lastStartTag != nil) { + let s = try tagPending.name() + return s.equalsIgnoreCase(string: lastStartTag!) + } + return false + } + + func appropriateEndTagName() -> String? { + if (lastStartTag == nil) { + return nil + } + return lastStartTag + } + + func error(_ state: TokeniserState) { + if (errors != nil && errors!.canAddError()) { + errors?.add(ParseError(reader.getPos(), "Unexpected character '\(String(reader.current()))' in input state [\(state.description)]")) + } + } + + func eofError(_ state: TokeniserState) { + if (errors != nil && errors!.canAddError()) { + errors?.add(ParseError(reader.getPos(), "Unexpectedly reached end of file (EOF) in input state [\(state.description)]")) + } + } + + private func characterReferenceError(_ message: String) { + if (errors != nil && errors!.canAddError()) { + errors?.add(ParseError(reader.getPos(), "Invalid character reference: \(message)")) + } + } + + private func error(_ errorMsg: String) { + if (errors != nil && errors!.canAddError()) { + errors?.add(ParseError(reader.getPos(), errorMsg)) + } + } + + func currentNodeInHtmlNS() -> Bool { + // todo: implement namespaces correctly + return true + // Element currentNode = currentNode() + // return currentNode != null && currentNode.namespace().equals("HTML") + } + + /** + * Utility method to consume reader and unescape entities found within. + * @param inAttribute + * @return unescaped string from reader + */ + func unescapeEntities(_ inAttribute: Bool)throws->String { + let builder: StringBuilder = StringBuilder() + while (!reader.isEmpty()) { + builder.append(reader.consumeTo(UnicodeScalar.Ampersand)) + if (reader.matches(UnicodeScalar.Ampersand)) { + reader.consume() + if let c = try consumeCharacterReference(nil, inAttribute) { + if (c.count==0) { + builder.append(UnicodeScalar.Ampersand) + } else { + builder.appendCodePoint(c[0]) + if (c.count == 2) { + builder.appendCodePoint(c[1]) + } + } + } else { + builder.append(UnicodeScalar.Ampersand) + } + } + } + return builder.toString() + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/TokeniserState.swift b/Swiftgram/SwiftSoup/Sources/TokeniserState.swift new file mode 100644 index 00000000000..707248a83bc --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/TokeniserState.swift @@ -0,0 +1,1644 @@ +// +// TokeniserState.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 12/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +protocol TokeniserStateProtocol { + func read(_ t: Tokeniser, _ r: CharacterReader)throws +} + +public class TokeniserStateVars { + public static let nullScalr: UnicodeScalar = "\u{0000}" + + static let attributeSingleValueCharsSorted = ["'", UnicodeScalar.Ampersand, nullScalr].sorted() + static let attributeDoubleValueCharsSorted = ["\"", UnicodeScalar.Ampersand, nullScalr].sorted() + static let attributeNameCharsSorted = [UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ", "/", "=", ">", nullScalr, "\"", "'", UnicodeScalar.LessThan].sorted() + static let attributeValueUnquoted = [UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ", UnicodeScalar.Ampersand, ">", nullScalr, "\"", "'", UnicodeScalar.LessThan, "=", "`"].sorted() + + static let replacementChar: UnicodeScalar = Tokeniser.replacementChar + static let replacementStr: String = String(Tokeniser.replacementChar) + static let eof: UnicodeScalar = CharacterReader.EOF +} + +enum TokeniserState: TokeniserStateProtocol { + case Data + case CharacterReferenceInData + case Rcdata + case CharacterReferenceInRcdata + case Rawtext + case ScriptData + case PLAINTEXT + case TagOpen + case EndTagOpen + case TagName + case RcdataLessthanSign + case RCDATAEndTagOpen + case RCDATAEndTagName + case RawtextLessthanSign + case RawtextEndTagOpen + case RawtextEndTagName + case ScriptDataLessthanSign + case ScriptDataEndTagOpen + case ScriptDataEndTagName + case ScriptDataEscapeStart + case ScriptDataEscapeStartDash + case ScriptDataEscaped + case ScriptDataEscapedDash + case ScriptDataEscapedDashDash + case ScriptDataEscapedLessthanSign + case ScriptDataEscapedEndTagOpen + case ScriptDataEscapedEndTagName + case ScriptDataDoubleEscapeStart + case ScriptDataDoubleEscaped + case ScriptDataDoubleEscapedDash + case ScriptDataDoubleEscapedDashDash + case ScriptDataDoubleEscapedLessthanSign + case ScriptDataDoubleEscapeEnd + case BeforeAttributeName + case AttributeName + case AfterAttributeName + case BeforeAttributeValue + case AttributeValue_doubleQuoted + case AttributeValue_singleQuoted + case AttributeValue_unquoted + case AfterAttributeValue_quoted + case SelfClosingStartTag + case BogusComment + case MarkupDeclarationOpen + case CommentStart + case CommentStartDash + case Comment + case CommentEndDash + case CommentEnd + case CommentEndBang + case Doctype + case BeforeDoctypeName + case DoctypeName + case AfterDoctypeName + case AfterDoctypePublicKeyword + case BeforeDoctypePublicIdentifier + case DoctypePublicIdentifier_doubleQuoted + case DoctypePublicIdentifier_singleQuoted + case AfterDoctypePublicIdentifier + case BetweenDoctypePublicAndSystemIdentifiers + case AfterDoctypeSystemKeyword + case BeforeDoctypeSystemIdentifier + case DoctypeSystemIdentifier_doubleQuoted + case DoctypeSystemIdentifier_singleQuoted + case AfterDoctypeSystemIdentifier + case BogusDoctype + case CdataSection + + internal func read(_ t: Tokeniser, _ r: CharacterReader)throws { + switch self { + case .Data: + switch (r.current()) { + case UnicodeScalar.Ampersand: + t.advanceTransition(.CharacterReferenceInData) + break + case UnicodeScalar.LessThan: + t.advanceTransition(.TagOpen) + break + case TokeniserStateVars.nullScalr: + t.error(self) // NOT replacement character (oddly?) + t.emit(r.consume()) + break + case TokeniserStateVars.eof: + try t.emit(Token.EOF()) + break + default: + let data: String = r.consumeData() + t.emit(data) + break + } + break + case .CharacterReferenceInData: + try TokeniserState.readCharRef(t, .Data) + break + case .Rcdata: + switch (r.current()) { + case UnicodeScalar.Ampersand: + t.advanceTransition(.CharacterReferenceInRcdata) + break + case UnicodeScalar.LessThan: + t.advanceTransition(.RcdataLessthanSign) + break + case TokeniserStateVars.nullScalr: + t.error(self) + r.advance() + t.emit(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + try t.emit(Token.EOF()) + break + default: + let data = r.consumeToAny(UnicodeScalar.Ampersand, UnicodeScalar.LessThan, TokeniserStateVars.nullScalr) + t.emit(data) + break + } + break + case .CharacterReferenceInRcdata: + try TokeniserState.readCharRef(t, .Rcdata) + break + case .Rawtext: + try TokeniserState.readData(t, r, self, .RawtextLessthanSign) + break + case .ScriptData: + try TokeniserState.readData(t, r, self, .ScriptDataLessthanSign) + break + case .PLAINTEXT: + switch (r.current()) { + case TokeniserStateVars.nullScalr: + t.error(self) + r.advance() + t.emit(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + try t.emit(Token.EOF()) + break + default: + let data = r.consumeTo(TokeniserStateVars.nullScalr) + t.emit(data) + break + } + break + case .TagOpen: + // from < in data + switch (r.current()) { + case "!": + t.advanceTransition(.MarkupDeclarationOpen) + break + case "/": + t.advanceTransition(.EndTagOpen) + break + case "?": + t.advanceTransition(.BogusComment) + break + default: + if (r.matchesLetter()) { + t.createTagPending(true) + t.transition(.TagName) + } else { + t.error(self) + t.emit(UnicodeScalar.LessThan) // char that got us here + t.transition(.Data) + } + break + } + break + case .EndTagOpen: + if (r.isEmpty()) { + t.eofError(self) + t.emit("")) { + t.error(self) + t.advanceTransition(.Data) + } else { + t.error(self) + t.advanceTransition(.BogusComment) + } + break + case .TagName: + // from < or ": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: // replacement + t.tagPending.appendTagName(TokeniserStateVars.replacementStr) + break + case TokeniserStateVars.eof: // should emit pending tag? + t.eofError(self) + t.transition(.Data) + // no default, as covered with above consumeToAny + default: + break + } + case .RcdataLessthanSign: + if (r.matches("/")) { + t.createTempBuffer() + t.advanceTransition(.RCDATAEndTagOpen) + } else if (r.matchesLetter() && t.appropriateEndTagName() != nil && !r.containsIgnoreCase("), so rather than + // consuming to EOF break out here + t.tagPending = t.createTagPending(false).name(t.appropriateEndTagName()!) + try t.emitTagPending() + r.unconsume() // undo UnicodeScalar.LessThan + t.transition(.Data) + } else { + t.emit(UnicodeScalar.LessThan) + t.transition(.Rcdata) + } + break + case .RCDATAEndTagOpen: + if (r.matchesLetter()) { + t.createTagPending(false) + t.tagPending.appendTagName(r.current()) + t.dataBuffer.append(r.current()) + t.advanceTransition(.RCDATAEndTagName) + } else { + t.emit("": + if (try t.isAppropriateEndTagToken()) { + try t.emitTagPending() + t.transition(.Data) + } else {anythingElse(t, r)} + break + default: + anythingElse(t, r) + break + } + break + case .RawtextLessthanSign: + if (r.matches("/")) { + t.createTempBuffer() + t.advanceTransition(.RawtextEndTagOpen) + } else { + t.emit(UnicodeScalar.LessThan) + t.transition(.Rawtext) + } + break + case .RawtextEndTagOpen: + TokeniserState.readEndTag(t, r, .RawtextEndTagName, .Rawtext) + break + case .RawtextEndTagName: + try TokeniserState.handleDataEndTag(t, r, .Rawtext) + break + case .ScriptDataLessthanSign: + switch (r.consume()) { + case "/": + t.createTempBuffer() + t.transition(.ScriptDataEndTagOpen) + break + case "!": + t.emit("": + t.emit(c) + t.transition(.ScriptData) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.emit(TokeniserStateVars.replacementChar) + t.transition(.ScriptDataEscaped) + break + default: + t.emit(c) + t.transition(.ScriptDataEscaped) + } + break + case .ScriptDataEscapedLessthanSign: + if (r.matchesLetter()) { + t.createTempBuffer() + t.dataBuffer.append(r.current()) + t.emit("<" + String(r.current())) + t.advanceTransition(.ScriptDataDoubleEscapeStart) + } else if (r.matches("/")) { + t.createTempBuffer() + t.advanceTransition(.ScriptDataEscapedEndTagOpen) + } else { + t.emit(UnicodeScalar.LessThan) + t.transition(.ScriptDataEscaped) + } + break + case .ScriptDataEscapedEndTagOpen: + if (r.matchesLetter()) { + t.createTagPending(false) + t.tagPending.appendTagName(r.current()) + t.dataBuffer.append(r.current()) + t.advanceTransition(.ScriptDataEscapedEndTagName) + } else { + t.emit("": + t.emit(c) + t.transition(.ScriptData) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.emit(TokeniserStateVars.replacementChar) + t.transition(.ScriptDataDoubleEscaped) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + default: + t.emit(c) + t.transition(.ScriptDataDoubleEscaped) + } + break + case .ScriptDataDoubleEscapedLessthanSign: + if (r.matches("/")) { + t.emit("/") + t.createTempBuffer() + t.advanceTransition(.ScriptDataDoubleEscapeEnd) + } else { + t.transition(.ScriptDataDoubleEscaped) + } + break + case .ScriptDataDoubleEscapeEnd: + TokeniserState.handleDataDoubleEscapeTag(t, r, .ScriptDataEscaped, .ScriptDataDoubleEscaped) + break + case .BeforeAttributeName: + // from tagname ": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + try t.tagPending.newAttribute() + r.unconsume() + t.transition(.AttributeName) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + case "\"", "'", UnicodeScalar.LessThan, "=": + t.error(self) + try t.tagPending.newAttribute() + t.tagPending.appendAttributeName(c) + t.transition(.AttributeName) + break + default: // A-Z, anything else + try t.tagPending.newAttribute() + r.unconsume() + t.transition(.AttributeName) + } + break + case .AttributeName: + let name = r.consumeToAnySorted(TokeniserStateVars.attributeNameCharsSorted) + t.tagPending.appendAttributeName(name) + + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT: + t.transition(.AfterAttributeName) + break + case "\n": + t.transition(.AfterAttributeName) + break + case "\r": + t.transition(.AfterAttributeName) + break + case UnicodeScalar.BackslashF: + t.transition(.AfterAttributeName) + break + case " ": + t.transition(.AfterAttributeName) + break + case "/": + t.transition(.SelfClosingStartTag) + break + case "=": + t.transition(.BeforeAttributeValue) + break + case ">": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeName(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + case "\"": + t.error(self) + t.tagPending.appendAttributeName(c) + case "'": + t.error(self) + t.tagPending.appendAttributeName(c) + case UnicodeScalar.LessThan: + t.error(self) + t.tagPending.appendAttributeName(c) + // no default, as covered in consumeToAny + default: + break + } + break + case .AfterAttributeName: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + // ignore + break + case "/": + t.transition(.SelfClosingStartTag) + break + case "=": + t.transition(.BeforeAttributeValue) + break + case ">": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeName(TokeniserStateVars.replacementChar) + t.transition(.AttributeName) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + case "\"", "'", UnicodeScalar.LessThan: + t.error(self) + try t.tagPending.newAttribute() + t.tagPending.appendAttributeName(c) + t.transition(.AttributeName) + break + default: // A-Z, anything else + try t.tagPending.newAttribute() + r.unconsume() + t.transition(.AttributeName) + } + break + case .BeforeAttributeValue: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + // ignore + break + case "\"": + t.transition(.AttributeValue_doubleQuoted) + break + case UnicodeScalar.Ampersand: + r.unconsume() + t.transition(.AttributeValue_unquoted) + break + case "'": + t.transition(.AttributeValue_singleQuoted) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeValue(TokeniserStateVars.replacementChar) + t.transition(.AttributeValue_unquoted) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitTagPending() + t.transition(.Data) + break + case ">": + t.error(self) + try t.emitTagPending() + t.transition(.Data) + break + case UnicodeScalar.LessThan, "=", "`": + t.error(self) + t.tagPending.appendAttributeValue(c) + t.transition(.AttributeValue_unquoted) + break + default: + r.unconsume() + t.transition(.AttributeValue_unquoted) + } + break + case .AttributeValue_doubleQuoted: + let value = r.consumeToAny(TokeniserStateVars.attributeDoubleValueCharsSorted) + if (value.count > 0) { + t.tagPending.appendAttributeValue(value) + } else { + t.tagPending.setEmptyAttributeValue() + } + + let c = r.consume() + switch (c) { + case "\"": + t.transition(.AfterAttributeValue_quoted) + break + case UnicodeScalar.Ampersand: + + if let ref = try t.consumeCharacterReference("\"", true) { + t.tagPending.appendAttributeValue(ref) + } else { + t.tagPending.appendAttributeValue(UnicodeScalar.Ampersand) + } + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeValue(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + // no default, handled in consume to any above + default: + break + } + break + case .AttributeValue_singleQuoted: + let value = r.consumeToAny(TokeniserStateVars.attributeSingleValueCharsSorted) + if (value.count > 0) { + t.tagPending.appendAttributeValue(value) + } else { + t.tagPending.setEmptyAttributeValue() + } + + let c = r.consume() + switch (c) { + case "'": + t.transition(.AfterAttributeValue_quoted) + break + case UnicodeScalar.Ampersand: + + if let ref = try t.consumeCharacterReference("'", true) { + t.tagPending.appendAttributeValue(ref) + } else { + t.tagPending.appendAttributeValue(UnicodeScalar.Ampersand) + } + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeValue(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + // no default, handled in consume to any above + default: + break + } + break + case .AttributeValue_unquoted: + let value = r.consumeToAnySorted(TokeniserStateVars.attributeValueUnquoted) + if (value.count > 0) { + t.tagPending.appendAttributeValue(value) + } + + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BeforeAttributeName) + break + case UnicodeScalar.Ampersand: + if let ref = try t.consumeCharacterReference(">", true) { + t.tagPending.appendAttributeValue(ref) + } else { + t.tagPending.appendAttributeValue(UnicodeScalar.Ampersand) + } + break + case ">": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.tagPending.appendAttributeValue(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + case "\"", "'", UnicodeScalar.LessThan, "=", "`": + t.error(self) + t.tagPending.appendAttributeValue(c) + break + // no default, handled in consume to any above + default: + break + } + break + case .AfterAttributeValue_quoted: + // CharacterReferenceInAttributeValue state handled inline + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BeforeAttributeName) + break + case "/": + t.transition(.SelfClosingStartTag) + break + case ">": + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + default: + t.error(self) + r.unconsume() + t.transition(.BeforeAttributeName) + } + break + case .SelfClosingStartTag: + let c = r.consume() + switch (c) { + case ">": + t.tagPending._selfClosing = true + try t.emitTagPending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.transition(.Data) + break + default: + t.error(self) + r.unconsume() + t.transition(.BeforeAttributeName) + } + break + case .BogusComment: + // todo: handle bogus comment starting from eof. when does that trigger? + // rewind to capture character that lead us here + r.unconsume() + let comment: Token.Comment = Token.Comment() + comment.bogus = true + comment.data.append(r.consumeTo(">")) + // todo: replace nullChar with replaceChar + try t.emit(comment) + t.advanceTransition(.Data) + break + case .MarkupDeclarationOpen: + if (r.matchConsume("--")) { + t.createCommentPending() + t.transition(.CommentStart) + } else if (r.matchConsumeIgnoreCase("DOCTYPE")) { + t.transition(.Doctype) + } else if (r.matchConsume("[CDATA[")) { + // todo: should actually check current namepspace, and only non-html allows cdata. until namespace + // is implemented properly, keep handling as cdata + //} else if (!t.currentNodeInHtmlNS() && r.matchConsume("[CDATA[")) { + t.transition(.CdataSection) + } else { + t.error(self) + t.advanceTransition(.BogusComment) // advance so self character gets in bogus comment data's rewind + } + break + case .CommentStart: + let c = r.consume() + switch (c) { + case "-": + t.transition(.CommentStartDash) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.commentPending.data.append(TokeniserStateVars.replacementChar) + t.transition(.Comment) + break + case ">": + t.error(self) + try t.emitCommentPending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.commentPending.data.append(c) + t.transition(.Comment) + } + break + case .CommentStartDash: + let c = r.consume() + switch (c) { + case "-": + t.transition(.CommentStartDash) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.commentPending.data.append(TokeniserStateVars.replacementChar) + t.transition(.Comment) + break + case ">": + t.error(self) + try t.emitCommentPending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.commentPending.data.append(c) + t.transition(.Comment) + } + break + case .Comment: + let c = r.current() + switch (c) { + case "-": + t.advanceTransition(.CommentEndDash) + break + case TokeniserStateVars.nullScalr: + t.error(self) + r.advance() + t.commentPending.data.append(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.commentPending.data.append(r.consumeToAny("-", TokeniserStateVars.nullScalr)) + } + break + case .CommentEndDash: + let c = r.consume() + switch (c) { + case "-": + t.transition(.CommentEnd) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.commentPending.data.append("-").append(TokeniserStateVars.replacementChar) + t.transition(.Comment) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.commentPending.data.append("-").append(c) + t.transition(.Comment) + } + break + case .CommentEnd: + let c = r.consume() + switch (c) { + case ">": + try t.emitCommentPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.commentPending.data.append("--").append(TokeniserStateVars.replacementChar) + t.transition(.Comment) + break + case "!": + t.error(self) + t.transition(.CommentEndBang) + break + case "-": + t.error(self) + t.commentPending.data.append("-") + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.error(self) + t.commentPending.data.append("--").append(c) + t.transition(.Comment) + } + break + case .CommentEndBang: + let c = r.consume() + switch (c) { + case "-": + t.commentPending.data.append("--!") + t.transition(.CommentEndDash) + break + case ">": + try t.emitCommentPending() + t.transition(.Data) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.commentPending.data.append("--!").append(TokeniserStateVars.replacementChar) + t.transition(.Comment) + break + case TokeniserStateVars.eof: + t.eofError(self) + try t.emitCommentPending() + t.transition(.Data) + break + default: + t.commentPending.data.append("--!").append(c) + t.transition(.Comment) + } + break + case .Doctype: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BeforeDoctypeName) + break + case TokeniserStateVars.eof: + t.eofError(self) + // note: fall through to > case + case ">": // catch invalid + t.error(self) + t.createDoctypePending() + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.transition(.BeforeDoctypeName) + } + break + case .BeforeDoctypeName: + if (r.matchesLetter()) { + t.createDoctypePending() + t.transition(.DoctypeName) + return + } + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + break // ignore whitespace + case TokeniserStateVars.nullScalr: + t.error(self) + t.createDoctypePending() + t.doctypePending.name.append(TokeniserStateVars.replacementChar) + t.transition(.DoctypeName) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.createDoctypePending() + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.createDoctypePending() + t.doctypePending.name.append(c) + t.transition(.DoctypeName) + } + break + case .DoctypeName: + if (r.matchesLetter()) { + let name = r.consumeLetterSequence() + t.doctypePending.name.append(name) + return + } + let c = r.consume() + switch (c) { + case ">": + try t.emitDoctypePending() + t.transition(.Data) + break + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.AfterDoctypeName) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.doctypePending.name.append(TokeniserStateVars.replacementChar) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.doctypePending.name.append(c) + } + break + case .AfterDoctypeName: + if (r.isEmpty()) { + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + return + } + if (r.matchesAny(UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ")) { + r.advance() // ignore whitespace + } else if (r.matches(">")) { + try t.emitDoctypePending() + t.advanceTransition(.Data) + } else if (r.matchConsumeIgnoreCase(DocumentType.PUBLIC_KEY)) { + t.doctypePending.pubSysKey = DocumentType.PUBLIC_KEY + t.transition(.AfterDoctypePublicKeyword) + } else if (r.matchConsumeIgnoreCase(DocumentType.SYSTEM_KEY)) { + t.doctypePending.pubSysKey = DocumentType.SYSTEM_KEY + t.transition(.AfterDoctypeSystemKeyword) + } else { + t.error(self) + t.doctypePending.forceQuirks = true + t.advanceTransition(.BogusDoctype) + } + break + case .AfterDoctypePublicKeyword: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BeforeDoctypePublicIdentifier) + break + case "\"": + t.error(self) + // set public id to empty string + t.transition(.DoctypePublicIdentifier_doubleQuoted) + break + case "'": + t.error(self) + // set public id to empty string + t.transition(.DoctypePublicIdentifier_singleQuoted) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + t.transition(.BogusDoctype) + } + break + case .BeforeDoctypePublicIdentifier: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + break + case "\"": + // set public id to empty string + t.transition(.DoctypePublicIdentifier_doubleQuoted) + break + case "'": + // set public id to empty string + t.transition(.DoctypePublicIdentifier_singleQuoted) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + t.transition(.BogusDoctype) + } + break + case .DoctypePublicIdentifier_doubleQuoted: + let c = r.consume() + switch (c) { + case "\"": + t.transition(.AfterDoctypePublicIdentifier) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.doctypePending.publicIdentifier.append(TokeniserStateVars.replacementChar) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.doctypePending.publicIdentifier.append(c) + } + break + case .DoctypePublicIdentifier_singleQuoted: + let c = r.consume() + switch (c) { + case "'": + t.transition(.AfterDoctypePublicIdentifier) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.doctypePending.publicIdentifier.append(TokeniserStateVars.replacementChar) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.doctypePending.publicIdentifier.append(c) + } + break + case .AfterDoctypePublicIdentifier: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BetweenDoctypePublicAndSystemIdentifiers) + break + case ">": + try t.emitDoctypePending() + t.transition(.Data) + break + case "\"": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_doubleQuoted) + break + case "'": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_singleQuoted) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + t.transition(.BogusDoctype) + } + break + case .BetweenDoctypePublicAndSystemIdentifiers: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + break + case ">": + try t.emitDoctypePending() + t.transition(.Data) + break + case "\"": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_doubleQuoted) + break + case "'": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_singleQuoted) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + t.transition(.BogusDoctype) + } + break + case .AfterDoctypeSystemKeyword: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(.BeforeDoctypeSystemIdentifier) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case "\"": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_doubleQuoted) + break + case "'": + t.error(self) + // system id empty + t.transition(.DoctypeSystemIdentifier_singleQuoted) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + } + break + case .BeforeDoctypeSystemIdentifier: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + break + case "\"": + // set system id to empty string + t.transition(.DoctypeSystemIdentifier_doubleQuoted) + break + case "'": + // set public id to empty string + t.transition(.DoctypeSystemIdentifier_singleQuoted) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.doctypePending.forceQuirks = true + t.transition(.BogusDoctype) + } + break + case .DoctypeSystemIdentifier_doubleQuoted: + let c = r.consume() + switch (c) { + case "\"": + t.transition(.AfterDoctypeSystemIdentifier) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.doctypePending.systemIdentifier.append(TokeniserStateVars.replacementChar) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.doctypePending.systemIdentifier.append(c) + } + break + case .DoctypeSystemIdentifier_singleQuoted: + let c = r.consume() + switch (c) { + case "'": + t.transition(.AfterDoctypeSystemIdentifier) + break + case TokeniserStateVars.nullScalr: + t.error(self) + t.doctypePending.systemIdentifier.append(TokeniserStateVars.replacementChar) + break + case ">": + t.error(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.doctypePending.systemIdentifier.append(c) + } + break + case .AfterDoctypeSystemIdentifier: + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + break + case ">": + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + t.eofError(self) + t.doctypePending.forceQuirks = true + try t.emitDoctypePending() + t.transition(.Data) + break + default: + t.error(self) + t.transition(.BogusDoctype) + // NOT force quirks + } + break + case .BogusDoctype: + let c = r.consume() + switch (c) { + case ">": + try t.emitDoctypePending() + t.transition(.Data) + break + case TokeniserStateVars.eof: + try t.emitDoctypePending() + t.transition(.Data) + break + default: + // ignore char + break + } + break + case .CdataSection: + let data = r.consumeTo("]]>") + t.emit(data) + r.matchConsume("]]>") + t.transition(.Data) + break + } + } + + var description: String {return String(describing: type(of: self))} + /** + * Handles RawtextEndTagName, ScriptDataEndTagName, and ScriptDataEscapedEndTagName. Same body impl, just + * different else exit transitions. + */ + private static func handleDataEndTag(_ t: Tokeniser, _ r: CharacterReader, _ elseTransition: TokeniserState)throws { + if (r.matchesLetter()) { + let name = r.consumeLetterSequence() + t.tagPending.appendTagName(name) + t.dataBuffer.append(name) + return + } + + var needsExitTransition = false + if (try t.isAppropriateEndTagToken() && !r.isEmpty()) { + let c = r.consume() + switch (c) { + case UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ": + t.transition(BeforeAttributeName) + break + case "/": + t.transition(SelfClosingStartTag) + break + case ">": + try t.emitTagPending() + t.transition(Data) + break + default: + t.dataBuffer.append(c) + needsExitTransition = true + } + } else { + needsExitTransition = true + } + + if (needsExitTransition) { + t.emit("": + if (t.dataBuffer.toString() == "script") { + t.transition(primary) + } else { + t.transition(fallback) + } + t.emit(c) + break + default: + r.unconsume() + t.transition(fallback) + } + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/TreeBuilder.swift b/Swiftgram/SwiftSoup/Sources/TreeBuilder.swift new file mode 100644 index 00000000000..a8b9ac0edea --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/TreeBuilder.swift @@ -0,0 +1,98 @@ +// +// TreeBuilder.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 24/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +public class TreeBuilder { + public var reader: CharacterReader + var tokeniser: Tokeniser + public var doc: Document // current doc we are building into + public var stack: Array // the stack of open elements + public var baseUri: String // current base uri, for creating new elements + public var currentToken: Token? // currentToken is used only for error tracking. + public var errors: ParseErrorList // null when not tracking errors + public var settings: ParseSettings + + private let start: Token.StartTag = Token.StartTag() // start tag to process + private let end: Token.EndTag = Token.EndTag() + + public func defaultSettings() -> ParseSettings {preconditionFailure("This method must be overridden")} + + public init() { + doc = Document("") + reader = CharacterReader("") + tokeniser = Tokeniser(reader, nil) + stack = Array() + baseUri = "" + errors = ParseErrorList(0, 0) + settings = ParseSettings(false, false) + } + + public func initialiseParse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings) { + doc = Document(baseUri) + self.settings = settings + reader = CharacterReader(input) + self.errors = errors + tokeniser = Tokeniser(reader, errors) + stack = Array() + self.baseUri = baseUri + } + + func parse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Document { + initialiseParse(input, baseUri, errors, settings) + try runParser() + return doc + } + + public func runParser()throws { + while (true) { + let token: Token = try tokeniser.read() + try process(token) + token.reset() + + if (token.type == Token.TokenType.EOF) { + break + } + } + } + + @discardableResult + public func process(_ token: Token)throws->Bool {preconditionFailure("This method must be overridden")} + + @discardableResult + public func processStartTag(_ name: String)throws->Bool { + if (currentToken === start) { // don't recycle an in-use token + return try process(Token.StartTag().name(name)) + } + return try process(start.reset().name(name)) + } + + @discardableResult + public func processStartTag(_ name: String, _ attrs: Attributes)throws->Bool { + if (currentToken === start) { // don't recycle an in-use token + return try process(Token.StartTag().nameAttr(name, attrs)) + } + start.reset() + start.nameAttr(name, attrs) + return try process(start) + } + + @discardableResult + public func processEndTag(_ name: String)throws->Bool { + if (currentToken === end) { // don't recycle an in-use token + return try process(Token.EndTag().name(name)) + } + + return try process(end.reset().name(name)) + } + + public func currentElement() -> Element? { + let size: Int = stack.count + return size > 0 ? stack[size-1] : nil + } +} diff --git a/Swiftgram/SwiftSoup/Sources/UnfairLock.swift b/Swiftgram/SwiftSoup/Sources/UnfairLock.swift new file mode 100644 index 00000000000..0ef99f0a42c --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/UnfairLock.swift @@ -0,0 +1,38 @@ +// +// UnfairLock.swift +// SwiftSoup +// +// Created by xukun on 2022/3/31. +// Copyright © 2022 Nabil Chatbi. All rights reserved. +// + +import Foundation + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) +@available(iOS 10.0, macOS 10.12, watchOS 3.0, tvOS 10.0, *) +final class UnfairLock: NSLocking { + + private let unfairLock: UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: os_unfair_lock()) + return pointer + }() + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + func lock() { + os_unfair_lock_lock(unfairLock) + } + + func tryLock() -> Bool { + return os_unfair_lock_trylock(unfairLock) + } + + func unlock() { + os_unfair_lock_unlock(unfairLock) + } +} +#endif diff --git a/Swiftgram/SwiftSoup/Sources/UnicodeScalar.swift b/Swiftgram/SwiftSoup/Sources/UnicodeScalar.swift new file mode 100644 index 00000000000..0a52709895f --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/UnicodeScalar.swift @@ -0,0 +1,67 @@ +// +// UnicodeScalar.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 14/11/16. +// Copyright © 2016 Nabil Chatbi. All rights reserved. +// + +import Foundation + +private let uppercaseSet = CharacterSet.uppercaseLetters +private let lowercaseSet = CharacterSet.lowercaseLetters +private let alphaSet = CharacterSet.letters +private let alphaNumericSet = CharacterSet.alphanumerics +private let symbolSet = CharacterSet.symbols +private let digitSet = CharacterSet.decimalDigits + +extension UnicodeScalar { + public static let Ampersand: UnicodeScalar = "&" + public static let LessThan: UnicodeScalar = "<" + public static let GreaterThan: UnicodeScalar = ">" + + public static let Space: UnicodeScalar = " " + public static let BackslashF: UnicodeScalar = UnicodeScalar(12) + public static let BackslashT: UnicodeScalar = "\t" + public static let BackslashN: UnicodeScalar = "\n" + public static let BackslashR: UnicodeScalar = "\r" + public static let Slash: UnicodeScalar = "/" + + public static let FormFeed: UnicodeScalar = "\u{000B}"// Form Feed + public static let VerticalTab: UnicodeScalar = "\u{000C}"// vertical tab + + func isMemberOfCharacterSet(_ set: CharacterSet) -> Bool { + return set.contains(self) + } + + /// True for any space character, and the control characters \t, \n, \r, \f, \v. + var isWhitespace: Bool { + + switch self { + + case UnicodeScalar.Space, UnicodeScalar.BackslashT, UnicodeScalar.BackslashN, UnicodeScalar.BackslashR, UnicodeScalar.BackslashF: return true + + case UnicodeScalar.FormFeed, UnicodeScalar.VerticalTab: return true // Form Feed, vertical tab + + default: return false + + } + + } + + /// `true` if `self` normalized contains a single code unit that is in the categories of Uppercase and Titlecase Letters. + var isUppercase: Bool { + return isMemberOfCharacterSet(uppercaseSet) + } + + /// `true` if `self` normalized contains a single code unit that is in the category of Lowercase Letters. + var isLowercase: Bool { + return isMemberOfCharacterSet(lowercaseSet) + + } + + var uppercase: UnicodeScalar { + let str = String(self).uppercased() + return str.unicodeScalar(0) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Validate.swift b/Swiftgram/SwiftSoup/Sources/Validate.swift new file mode 100644 index 00000000000..2e6e864e56c --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Validate.swift @@ -0,0 +1,133 @@ +// +// Validate.swift +// SwifSoup +// +// Created by Nabil Chatbi on 02/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +struct Validate { + + /** + * Validates that the object is not null + * @param obj object to test + */ + public static func notNull(obj: Any?) throws { + if (obj == nil) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Object must not be null") + } + } + + /** + * Validates that the object is not null + * @param obj object to test + * @param msg message to output if validation fails + */ + public static func notNull(obj: AnyObject?, msg: String) throws { + if (obj == nil) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + } + + /** + * Validates that the value is true + * @param val object to test + */ + public static func isTrue(val: Bool) throws { + if (!val) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Must be true") + } + } + + /** + * Validates that the value is true + * @param val object to test + * @param msg message to output if validation fails + */ + public static func isTrue(val: Bool, msg: String) throws { + if (!val) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + } + + /** + * Validates that the value is false + * @param val object to test + */ + public static func isFalse(val: Bool) throws { + if (val) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Must be false") + } + } + + /** + * Validates that the value is false + * @param val object to test + * @param msg message to output if validation fails + */ + public static func isFalse(val: Bool, msg: String) throws { + if (val) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + } + + /** + * Validates that the array contains no null elements + * @param objects the array to test + */ + public static func noNullElements(objects: [AnyObject?]) throws { + try noNullElements(objects: objects, msg: "Array must not contain any null objects") + } + + /** + * Validates that the array contains no null elements + * @param objects the array to test + * @param msg message to output if validation fails + */ + public static func noNullElements(objects: [AnyObject?], msg: String) throws { + for obj in objects { + if (obj == nil) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + } + } + + /** + * Validates that the string is not empty + * @param string the string to test + */ + public static func notEmpty(string: String?) throws { + if (string == nil || string?.count == 0) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "String must not be empty") + } + + } + + /** + * Validates that the string is not empty + * @param string the string to test + * @param msg message to output if validation fails + */ + public static func notEmpty(string: String?, msg: String ) throws { + if (string == nil || string?.count == 0) { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + } + + /** + Cause a failure. + @param msg message to output. + */ + public static func fail(msg: String) throws { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } + + /** + Helper + */ + public static func exception(msg: String) throws { + throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/Whitelist.swift b/Swiftgram/SwiftSoup/Sources/Whitelist.swift new file mode 100644 index 00000000000..c3951707680 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Whitelist.swift @@ -0,0 +1,650 @@ +// +// Whitelist.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 14/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +/* + Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired + this whitelist configuration, and the initial defaults. + */ + +/** + Whitelists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed. +

+ Start with one of the defaults: +

+
    +
  • {@link #none} +
  • {@link #simpleText} +
  • {@link #basic} +
  • {@link #basicWithImages} +
  • {@link #relaxed} +
+

+ If you need to allow more through (please be careful!), tweak a base whitelist with: +

+
    +
  • {@link #addTags} +
  • {@link #addAttributes} +
  • {@link #addEnforcedAttribute} +
  • {@link #addProtocols} +
+

+ You can remove any setting from an existing whitelist with: +

+
    +
  • {@link #removeTags} +
  • {@link #removeAttributes} +
  • {@link #removeEnforcedAttribute} +
  • {@link #removeProtocols} +
+ +

+ The cleaner and these whitelists assume that you want to clean a body fragment of HTML (to add user + supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the + document HTML around the cleaned body HTML, or create a whitelist that allows html and head + elements as appropriate. +

+

+ If you are going to extend a whitelist, please be very careful. Make sure you understand what attributes may lead to + XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See + http://ha.ckers.org/xss.html for some XSS attack examples. +

+ */ + +import Foundation + +public class Whitelist { + private var tagNames: Set // tags allowed, lower case. e.g. [p, br, span] + private var attributes: Dictionary> // tag -> attribute[]. allowed attributes [href] for a tag. + private var enforcedAttributes: Dictionary> // always set these attribute values + private var protocols: Dictionary>> // allowed URL protocols for attributes + private var preserveRelativeLinks: Bool // option to preserve relative links + + /** + This whitelist allows only text nodes: all HTML will be stripped. + + @return whitelist + */ + public static func none() -> Whitelist { + return Whitelist() + } + + /** + This whitelist allows only simple text formatting: b, em, i, strong, u. All other HTML (tags and + attributes) will be removed. + + @return whitelist + */ + public static func simpleText()throws ->Whitelist { + return try Whitelist().addTags("b", "em", "i", "strong", "u") + } + + /** +

+ This whitelist allows a fuller range of text nodes: a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, + ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul, and appropriate attributes. +

+

+ Links (a elements) can point to http, https, ftp, mailto, and have an enforced + rel=nofollow attribute. +

+

+ Does not allow images. +

+ + @return whitelist + */ + public static func basic()throws->Whitelist { + return try Whitelist() + .addTags( + "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em", + "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub", + "sup", "u", "ul") + + .addAttributes("a", "href") + .addAttributes("blockquote", "cite") + .addAttributes("q", "cite") + + .addProtocols("a", "href", "ftp", "http", "https", "mailto") + .addProtocols("blockquote", "cite", "http", "https") + .addProtocols("cite", "cite", "http", "https") + + .addEnforcedAttribute("a", "rel", "nofollow") + } + + /** + This whitelist allows the same text tags as {@link #basic}, and also allows img tags, with appropriate + attributes, with src pointing to http or https. + + @return whitelist + */ + public static func basicWithImages()throws->Whitelist { + return try basic() + .addTags("img") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addProtocols("img", "src", "http", "https") + + } + + /** + This whitelist allows a full range of text and structural body HTML: a, b, blockquote, br, caption, cite, + code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub, + sup, table, tbody, td, tfoot, th, thead, tr, u, ul +

+ Links do not have an enforced rel=nofollow attribute, but you can add that if desired. +

+ + @return whitelist + */ + public static func relaxed()throws->Whitelist { + return try Whitelist() + .addTags( + "a", "b", "blockquote", "br", "caption", "cite", "code", "col", + "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", + "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", + "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", + "ul") + + .addAttributes("a", "href", "title") + .addAttributes("blockquote", "cite") + .addAttributes("col", "span", "width") + .addAttributes("colgroup", "span", "width") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addAttributes("ol", "start", "type") + .addAttributes("q", "cite") + .addAttributes("table", "summary", "width") + .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width") + .addAttributes( + "th", "abbr", "axis", "colspan", "rowspan", "scope", + "width") + .addAttributes("ul", "type") + + .addProtocols("a", "href", "ftp", "http", "https", "mailto") + .addProtocols("blockquote", "cite", "http", "https") + .addProtocols("cite", "cite", "http", "https") + .addProtocols("img", "src", "http", "https") + .addProtocols("q", "cite", "http", "https") + } + + /** + Create a new, empty whitelist. Generally it will be better to start with a default prepared whitelist instead. + + @see #basic() + @see #basicWithImages() + @see #simpleText() + @see #relaxed() + */ + init() { + tagNames = Set() + attributes = Dictionary>() + enforcedAttributes = Dictionary>() + protocols = Dictionary>>() + preserveRelativeLinks = false + } + + /** + Add a list of allowed elements to a whitelist. (If a tag is not allowed, it will be removed from the HTML.) + + @param tags tag names to allow + @return this (for chaining) + */ + @discardableResult + open func addTags(_ tags: String...)throws ->Whitelist { + for tagName in tags { + try Validate.notEmpty(string: tagName) + tagNames.insert(TagName.valueOf(tagName)) + } + return self + } + + /** + Remove a list of allowed elements from a whitelist. (If a tag is not allowed, it will be removed from the HTML.) + + @param tags tag names to disallow + @return this (for chaining) + */ + @discardableResult + open func removeTags(_ tags: String...)throws ->Whitelist { + try Validate.notNull(obj: tags) + + for tag in tags { + try Validate.notEmpty(string: tag) + let tagName: TagName = TagName.valueOf(tag) + + if(tagNames.contains(tagName)) { // Only look in sub-maps if tag was allowed + tagNames.remove(tagName) + attributes.removeValue(forKey: tagName) + enforcedAttributes.removeValue(forKey: tagName) + protocols.removeValue(forKey: tagName) + } + } + return self + } + + /** + Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.) +

+ E.g.: addAttributes("a", "href", "class") allows href and class attributes + on a tags. +

+

+ To make an attribute valid for all tags, use the pseudo tag :all, e.g. + addAttributes(":all", "class"). +

+ + @param tag The tag the attributes are for. The tag will be added to the allowed tag list if necessary. + @param keys List of valid attributes for the tag + @return this (for chaining) + */ + @discardableResult + open func addAttributes(_ tag: String, _ keys: String...)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.isTrue(val: keys.count > 0, msg: "No attributes supplied.") + + let tagName = TagName.valueOf(tag) + if (!tagNames.contains(tagName)) { + tagNames.insert(tagName) + } + var attributeSet = Set() + for key in keys { + try Validate.notEmpty(string: key) + attributeSet.insert(AttributeKey.valueOf(key)) + } + + if var currentSet = attributes[tagName] { + for at in attributeSet { + currentSet.insert(at) + } + attributes[tagName] = currentSet + } else { + attributes[tagName] = attributeSet + } + + return self + } + + /** + Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.) +

+ E.g.: removeAttributes("a", "href", "class") disallows href and class + attributes on a tags. +

+

+ To make an attribute invalid for all tags, use the pseudo tag :all, e.g. + removeAttributes(":all", "class"). +

+ + @param tag The tag the attributes are for. + @param keys List of invalid attributes for the tag + @return this (for chaining) + */ + @discardableResult + open func removeAttributes(_ tag: String, _ keys: String...)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.isTrue(val: keys.count > 0, msg: "No attributes supplied.") + + let tagName: TagName = TagName.valueOf(tag) + var attributeSet = Set() + for key in keys { + try Validate.notEmpty(string: key) + attributeSet.insert(AttributeKey.valueOf(key)) + } + + if(tagNames.contains(tagName)) { // Only look in sub-maps if tag was allowed + if var currentSet = attributes[tagName] { + for l in attributeSet { + currentSet.remove(l) + } + attributes[tagName] = currentSet + if(currentSet.isEmpty) { // Remove tag from attribute map if no attributes are allowed for tag + attributes.removeValue(forKey: tagName) + } + } + + } + + if(tag == ":all") { // Attribute needs to be removed from all individually set tags + for name in attributes.keys { + var currentSet: Set = attributes[name]! + for l in attributeSet { + currentSet.remove(l) + } + attributes[name] = currentSet + if(currentSet.isEmpty) { // Remove tag from attribute map if no attributes are allowed for tag + attributes.removeValue(forKey: name) + } + } + } + return self + } + + /** + Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element + already has the attribute set, it will be overridden. +

+ E.g.: addEnforcedAttribute("a", "rel", "nofollow") will make all a tags output as + <a href="..." rel="nofollow"> +

+ + @param tag The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary. + @param key The attribute key + @param value The enforced attribute value + @return this (for chaining) + */ + @discardableResult + open func addEnforcedAttribute(_ tag: String, _ key: String, _ value: String)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.notEmpty(string: key) + try Validate.notEmpty(string: value) + + let tagName: TagName = TagName.valueOf(tag) + if (!tagNames.contains(tagName)) { + tagNames.insert(tagName) + } + let attrKey: AttributeKey = AttributeKey.valueOf(key) + let attrVal: AttributeValue = AttributeValue.valueOf(value) + + if (enforcedAttributes[tagName] != nil) { + enforcedAttributes[tagName]?[attrKey] = attrVal + } else { + var attrMap: Dictionary = Dictionary() + attrMap[attrKey] = attrVal + enforcedAttributes[tagName] = attrMap + } + return self + } + + /** + Remove a previously configured enforced attribute from a tag. + + @param tag The tag the enforced attribute is for. + @param key The attribute key + @return this (for chaining) + */ + @discardableResult + open func removeEnforcedAttribute(_ tag: String, _ key: String)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.notEmpty(string: key) + + let tagName: TagName = TagName.valueOf(tag) + if(tagNames.contains(tagName) && (enforcedAttributes[tagName] != nil)) { + let attrKey: AttributeKey = AttributeKey.valueOf(key) + var attrMap: Dictionary = enforcedAttributes[tagName]! + attrMap.removeValue(forKey: attrKey) + enforcedAttributes[tagName] = attrMap + + if(attrMap.isEmpty) { // Remove tag from enforced attribute map if no enforced attributes are present + enforcedAttributes.removeValue(forKey: tagName) + } + } + return self + } + + /** + * Configure this Whitelist to preserve relative links in an element's URL attribute, or convert them to absolute + * links. By default, this is false: URLs will be made absolute (e.g. start with an allowed protocol, like + * e.g. {@code http://}. + *

+ * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when + * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative + * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute + * will be removed. + *

+ * + * @param preserve {@code true} to allow relative links, {@code false} (default) to deny + * @return this Whitelist, for chaining. + * @see #addProtocols + */ + @discardableResult + open func preserveRelativeLinks(_ preserve: Bool) -> Whitelist { + preserveRelativeLinks = preserve + return self + } + + /** + Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to + URLs with the defined protocol. +

+ E.g.: addProtocols("a", "href", "ftp", "http", "https") +

+

+ To allow a link to an in-page URL anchor (i.e. <a href="#anchor">, add a #:
+ E.g.: addProtocols("a", "href", "#") +

+ + @param tag Tag the URL protocol is for + @param key Attribute key + @param protocols List of valid protocols + @return this, for chaining + */ + @discardableResult + open func addProtocols(_ tag: String, _ key: String, _ protocols: String...)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.notEmpty(string: key) + + let tagName: TagName = TagName.valueOf(tag) + let attrKey: AttributeKey = AttributeKey.valueOf(key) + var attrMap: Dictionary> + var protSet: Set + + if (self.protocols[tagName] != nil) { + attrMap = self.protocols[tagName]! + } else { + attrMap = Dictionary>() + self.protocols[tagName] = attrMap + } + + if (attrMap[attrKey] != nil) { + protSet = attrMap[attrKey]! + } else { + protSet = Set() + attrMap[attrKey] = protSet + self.protocols[tagName] = attrMap + } + for ptl in protocols { + try Validate.notEmpty(string: ptl) + let prot: Protocol = Protocol.valueOf(ptl) + protSet.insert(prot) + } + attrMap[attrKey] = protSet + self.protocols[tagName] = attrMap + + return self + } + + /** + Remove allowed URL protocols for an element's URL attribute. +

+ E.g.: removeProtocols("a", "href", "ftp") +

+ + @param tag Tag the URL protocol is for + @param key Attribute key + @param protocols List of invalid protocols + @return this, for chaining + */ + @discardableResult + open func removeProtocols(_ tag: String, _ key: String, _ protocols: String...)throws->Whitelist { + try Validate.notEmpty(string: tag) + try Validate.notEmpty(string: key) + + let tagName: TagName = TagName.valueOf(tag) + let attrKey: AttributeKey = AttributeKey.valueOf(key) + + if(self.protocols[tagName] != nil) { + var attrMap: Dictionary> = self.protocols[tagName]! + if(attrMap[attrKey] != nil) { + var protSet: Set = attrMap[attrKey]! + for ptl in protocols { + try Validate.notEmpty(string: ptl) + let prot: Protocol = Protocol.valueOf(ptl) + protSet.remove(prot) + } + attrMap[attrKey] = protSet + + if(protSet.isEmpty) { // Remove protocol set if empty + attrMap.removeValue(forKey: attrKey) + if(attrMap.isEmpty) { // Remove entry for tag if empty + self.protocols.removeValue(forKey: tagName) + } + + } + } + self.protocols[tagName] = attrMap + } + return self + } + + /** + * Test if the supplied tag is allowed by this whitelist + * @param tag test tag + * @return true if allowed + */ + public func isSafeTag(_ tag: String) -> Bool { + return tagNames.contains(TagName.valueOf(tag)) + } + + /** + * Test if the supplied attribute is allowed by this whitelist for this tag + * @param tagName tag to consider allowing the attribute in + * @param el element under test, to confirm protocol + * @param attr attribute under test + * @return true if allowed + */ + public func isSafeAttribute(_ tagName: String, _ el: Element, _ attr: Attribute)throws -> Bool { + let tag: TagName = TagName.valueOf(tagName) + let key: AttributeKey = AttributeKey.valueOf(attr.getKey()) + + if (attributes[tag] != nil) { + if (attributes[tag]?.contains(key))! { + if (protocols[tag] != nil) { + let attrProts: Dictionary> = protocols[tag]! + // ok if not defined protocol; otherwise test + return try (attrProts[key] == nil) || testValidProtocol(el, attr, attrProts[key]!) + } else { // attribute found, no protocols defined, so OK + return true + } + } + } + // no attributes defined for tag, try :all tag + return try !(tagName == ":all") && isSafeAttribute(":all", el, attr) + } + + private func testValidProtocol(_ el: Element, _ attr: Attribute, _ protocols: Set)throws->Bool { + // try to resolve relative urls to abs, and optionally update the attribute so output html has abs. + // rels without a baseuri get removed + var value: String = try el.absUrl(attr.getKey()) + if (value.count == 0) { + value = attr.getValue() + }// if it could not be made abs, run as-is to allow custom unknown protocols + if (!preserveRelativeLinks) { + attr.setValue(value: value) + } + + for ptl in protocols { + var prot: String = ptl.toString() + + if (prot=="#") { // allows anchor links + if (isValidAnchor(value)) { + return true + } else { + continue + } + } + + prot += ":" + + if (value.lowercased().hasPrefix(prot)) { + return true + } + + } + + return false + } + + private func isValidAnchor(_ value: String) -> Bool { + return value.startsWith("#") && !(Pattern(".*\\s.*").matcher(in: value).count > 0) + } + + public func getEnforcedAttributes(_ tagName: String)throws->Attributes { + let attrs: Attributes = Attributes() + let tag: TagName = TagName.valueOf(tagName) + if let keyVals: Dictionary = enforcedAttributes[tag] { + for entry in keyVals { + try attrs.put(entry.key.toString(), entry.value.toString()) + } + } + return attrs + } + +} + +// named types for config. All just hold strings, but here for my sanity. + +open class TagName: TypedValue { + override init(_ value: String) { + super.init(value) + } + + static func valueOf(_ value: String) -> TagName { + return TagName(value) + } +} + +open class AttributeKey: TypedValue { + override init(_ value: String) { + super.init(value) + } + + static func valueOf(_ value: String) -> AttributeKey { + return AttributeKey(value) + } +} + +open class AttributeValue: TypedValue { + override init(_ value: String) { + super.init(value) + } + + static func valueOf(_ value: String) -> AttributeValue { + return AttributeValue(value) + } +} + +open class Protocol: TypedValue { + override init(_ value: String) { + super.init(value) + } + + static func valueOf(_ value: String) -> Protocol { + return Protocol(value) + } +} + +open class TypedValue { + fileprivate let value: String + + init(_ value: String) { + self.value = value + } + + public func toString() -> String { + return value + } +} + +extension TypedValue: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +public func == (lhs: TypedValue, rhs: TypedValue) -> Bool { + if(lhs === rhs) {return true} + return lhs.value == rhs.value +} diff --git a/Swiftgram/SwiftSoup/Sources/XmlDeclaration.swift b/Swiftgram/SwiftSoup/Sources/XmlDeclaration.swift new file mode 100644 index 00000000000..5f1032b6ab5 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/XmlDeclaration.swift @@ -0,0 +1,77 @@ +// +// XmlDeclaration.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + An XML Declaration. + */ +public class XmlDeclaration: Node { + private let _name: String + private let isProcessingInstruction: Bool // String { + return "#declaration" + } + + /** + * Get the name of this declaration. + * @return name of this declaration. + */ + public func name() -> String { + return _name + } + + /** + Get the unencoded XML declaration. + @return XML declaration + */ + public func getWholeDeclaration()throws->String { + return try attributes!.html().trim() // attr html starts with a " " + } + + override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { + accum + .append("<") + .append(isProcessingInstruction ? "!" : "?") + .append(_name) + do { + try attributes?.html(accum: accum, out: out) + } catch {} + accum + .append(isProcessingInstruction ? "!" : "?") + .append(">") + } + + override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {} + + public override func copy(with zone: NSZone? = nil) -> Any { + let clone = XmlDeclaration(_name, baseUri!, isProcessingInstruction) + return copy(clone: clone) + } + + public override func copy(parent: Node?) -> Node { + let clone = XmlDeclaration(_name, baseUri!, isProcessingInstruction) + return copy(clone: clone, parent: parent) + } + public override func copy(clone: Node, parent: Node?) -> Node { + return super.copy(clone: clone, parent: parent) + } +} diff --git a/Swiftgram/SwiftSoup/Sources/XmlTreeBuilder.swift b/Swiftgram/SwiftSoup/Sources/XmlTreeBuilder.swift new file mode 100644 index 00000000000..785a68b84c5 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/XmlTreeBuilder.swift @@ -0,0 +1,146 @@ +// +// XmlTreeBuilder.swift +// SwiftSoup +// +// Created by Nabil Chatbi on 14/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * Use the {@code XmlTreeBuilder} when you want to parse XML without any of the HTML DOM rules being applied to the + * document. + *

Usage example: {@code Document xmlDoc = Jsoup.parse(html, baseUrl, Parser.xmlParser())}

+ * + */ +public class XmlTreeBuilder: TreeBuilder { + + public override init() { + super.init() + } + + public override func defaultSettings() -> ParseSettings { + return ParseSettings.preserveCase + } + + public func parse(_ input: String, _ baseUri: String)throws->Document { + return try parse(input, baseUri, ParseErrorList.noTracking(), ParseSettings.preserveCase) + } + + override public func initialiseParse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings) { + super.initialiseParse(input, baseUri, errors, settings) + stack.append(doc) // place the document onto the stack. differs from HtmlTreeBuilder (not on stack) + doc.outputSettings().syntax(syntax: OutputSettings.Syntax.xml) + } + + override public func process(_ token: Token)throws->Bool { + // start tag, end tag, doctype, comment, character, eof + switch (token.type) { + case .StartTag: + try insert(token.asStartTag()) + break + case .EndTag: + try popStackToClose(token.asEndTag()) + break + case .Comment: + try insert(token.asComment()) + break + case .Char: + try insert(token.asCharacter()) + break + case .Doctype: + try insert(token.asDoctype()) + break + case .EOF: // could put some normalisation here if desired + break +// default: +// try Validate.fail(msg: "Unexpected token type: " + token.tokenType()) + } + return true + } + + private func insertNode(_ node: Node)throws { + try currentElement()?.appendChild(node) + } + + @discardableResult + func insert(_ startTag: Token.StartTag)throws->Element { + let tag: Tag = try Tag.valueOf(startTag.name(), settings) + // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html. + let el: Element = try Element(tag, baseUri, settings.normalizeAttributes(startTag._attributes)) + try insertNode(el) + if (startTag.isSelfClosing()) { + tokeniser.acknowledgeSelfClosingFlag() + if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above. + { + tag.setSelfClosing() + } + } else { + stack.append(el) + } + return el + } + + func insert(_ commentToken: Token.Comment)throws { + let comment: Comment = Comment(commentToken.getData(), baseUri) + var insert: Node = comment + if (commentToken.bogus) { // xml declarations are emitted as bogus comments (which is right for html, but not xml) + // so we do a bit of a hack and parse the data as an element to pull the attributes out + let data: String = comment.getData() + if (data.count > 1 && (data.startsWith("!") || data.startsWith("?"))) { + let doc: Document = try SwiftSoup.parse("<" + data.substring(1, data.count - 2) + ">", baseUri, Parser.xmlParser()) + let el: Element = doc.child(0) + insert = XmlDeclaration(settings.normalizeTag(el.tagName()), comment.getBaseUri(), data.startsWith("!")) + insert.getAttributes()?.addAll(incoming: el.getAttributes()) + } + } + try insertNode(insert) + } + + func insert(_ characterToken: Token.Char)throws { + let node: Node = TextNode(characterToken.getData()!, baseUri) + try insertNode(node) + } + + func insert(_ d: Token.Doctype)throws { + let doctypeNode = DocumentType(settings.normalizeTag(d.getName()), d.getPubSysKey(), d.getPublicIdentifier(), d.getSystemIdentifier(), baseUri) + try insertNode(doctypeNode) + } + + /** + * If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not + * found, skips. + * + * @param endTag + */ + private func popStackToClose(_ endTag: Token.EndTag)throws { + let elName: String = try endTag.name() + var firstFound: Element? = nil + + for pos in (0..Array { + initialiseParse(inputFragment, baseUri, errors, settings) + try runParser() + return doc.getChildNodes() + } +} diff --git a/Swiftgram/Wrap/BUILD b/Swiftgram/Wrap/BUILD new file mode 100644 index 00000000000..2a1b4a85784 --- /dev/null +++ b/Swiftgram/Wrap/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Wrap", + module_name = "Wrap", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/Wrap/Sources/Wrap.swift b/Swiftgram/Wrap/Sources/Wrap.swift new file mode 100644 index 00000000000..055ab2b8754 --- /dev/null +++ b/Swiftgram/Wrap/Sources/Wrap.swift @@ -0,0 +1,568 @@ +/** + * Wrap - the easy to use Swift JSON encoder + * + * For usage, see documentation of the classes/symbols listed in this file, as well + * as the guide available at: github.com/johnsundell/wrap + * + * Copyright (c) 2015 - 2017 John Sundell. Licensed under the MIT license, as follows: + * + * 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 + +/// Type alias defining what type of Dictionary that Wrap produces +public typealias WrappedDictionary = [String : Any] + +/** + * Wrap any object or value, encoding it into a JSON compatible Dictionary + * + * - Parameter object: The object to encode + * - Parameter context: An optional contextual object that will be available throughout + * the wrapping process. Can be used to inject extra information or objects needed to + * perform the wrapping. + * - Parameter dateFormatter: Optionally pass in a date formatter to use to encode any + * `NSDate` values found while encoding the object. If this is `nil`, any found date + * values will be encoded using the "yyyy-MM-dd HH:mm:ss" format. + * + * All the type's stored properties (both public & private) will be recursively + * encoded with their property names as the key. For example, given the following + * Struct as input: + * + * ``` + * struct User { + * let name = "John" + * let age = 28 + * } + * ``` + * + * This function will produce the following output: + * + * ``` + * [ + * "name" : "John", + * "age" : 28 + * ] + * ``` + * + * The object passed to this function must be an instance of a Class, or a value + * based on a Struct. Standard library values, such as Ints, Strings, etc are not + * valid input. + * + * Throws a WrapError if the operation could not be completed. + * + * For more customization options, make your type conform to `WrapCustomizable`, + * that lets you override encoding keys and/or the whole wrapping process. + * + * See also `WrappableKey` (for dictionary keys) and `WrappableEnum` for Enum values. + */ +public func wrap(_ object: T, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> WrappedDictionary { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, enableCustomizedWrapping: true) +} + +/** + * Alternative `wrap()` overload that returns JSON-based `Data` + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ object: T, writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, writingOptions: writingOptions ?? []) +} + +/** + * Alternative `wrap()` overload that encodes an array of objects into an array of dictionaries + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ objects: [T], context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> [WrappedDictionary] { + return try objects.map { try wrap($0, context: context, dateFormatter: dateFormatter) } +} + +/** + * Alternative `wrap()` overload that encodes an array of objects into JSON-based `Data` + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ objects: [T], writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { + let dictionaries: [WrappedDictionary] = try wrap(objects, context: context, dateFormatter: dateFormatter) + return try JSONSerialization.data(withJSONObject: dictionaries, options: writingOptions ?? []) +} + +// Enum describing various styles of keys in a wrapped dictionary +public enum WrapKeyStyle { + /// The keys in a dictionary produced by Wrap should match their property name (default) + case matchPropertyName + /// The keys in a dictionary produced by Wrap should be converted to snake_case. + /// For example, "myProperty" will be converted to "my_property". All keys will be lowercased. + case convertToSnakeCase +} + +/** + * Protocol providing the main customization point for Wrap + * + * It's optional to implement all of the methods in this protocol, as Wrap + * supplies default implementations of them. + */ +public protocol WrapCustomizable { + /** + * The style that wrap should apply to the keys of a wrapped dictionary + * + * The value of this property is ignored if a type provides a custom + * implementation of the `keyForWrapping(propertyNamed:)` method. + */ + var wrapKeyStyle: WrapKeyStyle { get } + /** + * Override the wrapping process for this type + * + * All top-level types should return a `WrappedDictionary` from this method. + * + * You may use the default wrapping implementation by using a `Wrapper`, but + * never call `wrap()` from an implementation of this method, since that might + * cause an infinite recursion. + * + * The context & dateFormatter passed to this method is any formatter that you + * supplied when initiating the wrapping process by calling `wrap()`. + * + * Returning nil from this method will be treated as an error, and cause + * a `WrapError.wrappingFailedForObject()` error to be thrown. + */ + func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? + /** + * Override the key that will be used when encoding a certain property + * + * Returning nil from this method will cause Wrap to skip the property + */ + func keyForWrapping(propertyNamed propertyName: String) -> String? + /** + * Override the wrapping of any property of this type + * + * The original value passed to this method will be the original value that the + * type is currently storing for the property. You can choose to either use this, + * or just access the property in question directly. + * + * The dateFormatter passed to this method is any formatter that you supplied + * when initiating the wrapping process by calling `wrap()`. + * + * Returning nil from this method will cause Wrap to use the default + * wrapping mechanism for the property, so you can choose which properties + * you want to customize the wrapping for. + * + * If you encounter an error while attempting to wrap the property in question, + * you can choose to throw. This will cause a WrapError.WrappingFailedForObject + * to be thrown from the main `wrap()` call that started the process. + */ + func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? +} + +/// Protocol implemented by types that may be used as keys in a wrapped Dictionary +public protocol WrappableKey { + /// Convert this type into a key that can be used in a wrapped Dictionary + func toWrappedKey() -> String +} + +/** + * Protocol implemented by Enums to enable them to be directly wrapped + * + * If an Enum implementing this protocol conforms to `RawRepresentable` (it's based + * on a raw type), no further implementation is required. If you wish to customize + * how the Enum is wrapped, you can use the APIs in `WrapCustomizable`. + */ +public protocol WrappableEnum: WrapCustomizable {} + +/// Protocol implemented by Date types to enable them to be wrapped +public protocol WrappableDate { + /// Wrap the date using a date formatter, generating a string representation + func wrap(dateFormatter: DateFormatter) -> String +} + +/** + * Class used to wrap an object or value. Use this in any custom `wrap()` implementations + * in case you only want to add on top of the default implementation. + * + * You normally don't have to interact with this API. Use the `wrap()` function instead + * to wrap an object from top-level code. + */ +public class Wrapper { + fileprivate let context: Any? + fileprivate var dateFormatter: DateFormatter? + + /** + * Initialize an instance of this class + * + * - Parameter context: An optional contextual object that will be available throughout the + * wrapping process. Can be used to inject extra information or objects needed to perform + * the wrapping. + * - Parameter dateFormatter: Any specific date formatter to use to encode any found `NSDate` + * values. If this is `nil`, any found date values will be encoded using the "yyyy-MM-dd + * HH:mm:ss" format. + */ + public init(context: Any? = nil, dateFormatter: DateFormatter? = nil) { + self.context = context + self.dateFormatter = dateFormatter + } + + /// Perform automatic wrapping of an object or value. For more information, see `Wrap()`. + public func wrap(object: Any) throws -> WrappedDictionary { + return try self.wrap(object: object, enableCustomizedWrapping: false) + } +} + +/// Error type used by Wrap +public enum WrapError: Error { + /// Thrown when an invalid top level object (such as a String or Int) was passed to `Wrap()` + case invalidTopLevelObject(Any) + /// Thrown when an object couldn't be wrapped. This is a last resort error. + case wrappingFailedForObject(Any) +} + +// MARK: - Default protocol implementations + +/// Extension containing default implementations of `WrapCustomizable`. Override as you see fit. +public extension WrapCustomizable { + var wrapKeyStyle: WrapKeyStyle { + return .matchPropertyName + } + + func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) + } + + func keyForWrapping(propertyNamed propertyName: String) -> String? { + switch self.wrapKeyStyle { + case .matchPropertyName: + return propertyName + case .convertToSnakeCase: + return self.convertPropertyNameToSnakeCase(propertyName: propertyName) + } + } + + func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(value: originalValue, propertyName: propertyName) + } +} + +/// Extension adding convenience APIs to `WrapCustomizable` types +public extension WrapCustomizable { + /// Convert a given property name (assumed to be camelCased) to snake_case + func convertPropertyNameToSnakeCase(propertyName: String) -> String { + let regex = try! NSRegularExpression(pattern: "(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])", options: []) + let range = NSRange(location: 0, length: propertyName.count) + let camelCasePropertyName = regex.stringByReplacingMatches(in: propertyName, options: [], range: range, withTemplate: "_$1$2") + return camelCasePropertyName.lowercased() + } +} + +/// Extension providing a default wrapping implementation for `RawRepresentable` Enums +public extension WrappableEnum where Self: RawRepresentable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.rawValue + } +} + +/// Extension customizing how Arrays are wrapped +extension Array: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) + } +} + +/// Extension customizing how Dictionaries are wrapped +extension Dictionary: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self) + } +} + +/// Extension customizing how Sets are wrapped +extension Set: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) + } +} + +/// Extension customizing how Int64s are wrapped, ensuring compatbility with 32 bit systems +extension Int64: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return NSNumber(value: self) + } +} + +/// Extension customizing how UInt64s are wrapped, ensuring compatbility with 32 bit systems +extension UInt64: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return NSNumber(value: self) + } +} + +/// Extension customizing how NSStrings are wrapped +extension NSString: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self + } +} + +/// Extension customizing how NSURLs are wrapped +extension NSURL: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.absoluteString + } +} + +/// Extension customizing how URLs are wrapped +extension URL: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.absoluteString + } +} + + +/// Extension customizing how NSArrays are wrapped +extension NSArray: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: Array(self)) + } +} + +#if !os(Linux) +/// Extension customizing how NSDictionaries are wrapped +extension NSDictionary: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self as [NSObject : AnyObject]) + } +} +#endif + +/// Extension making Int a WrappableKey +extension Int: WrappableKey { + public func toWrappedKey() -> String { + return String(self) + } +} + +/// Extension making Date a WrappableDate +extension Date: WrappableDate { + public func wrap(dateFormatter: DateFormatter) -> String { + return dateFormatter.string(from: self) + } +} + +#if !os(Linux) +/// Extension making NSdate a WrappableDate +extension NSDate: WrappableDate { + public func wrap(dateFormatter: DateFormatter) -> String { + return dateFormatter.string(from: self as Date) + } +} +#endif + +// MARK: - Private + +private extension Wrapper { + func wrap(object: T, enableCustomizedWrapping: Bool) throws -> WrappedDictionary { + if enableCustomizedWrapping { + if let customizable = object as? WrapCustomizable { + let wrapped = try self.performCustomWrapping(object: customizable) + + guard let wrappedDictionary = wrapped as? WrappedDictionary else { + throw WrapError.invalidTopLevelObject(object) + } + + return wrappedDictionary + } + } + + var mirrors = [Mirror]() + var currentMirror: Mirror? = Mirror(reflecting: object) + + while let mirror = currentMirror { + mirrors.append(mirror) + currentMirror = mirror.superclassMirror + } + + return try self.performWrapping(object: object, mirrors: mirrors.reversed()) + } + + func wrap(object: T, writingOptions: JSONSerialization.WritingOptions) throws -> Data { + let dictionary = try self.wrap(object: object, enableCustomizedWrapping: true) + return try JSONSerialization.data(withJSONObject: dictionary, options: writingOptions) + } + + func wrap(value: T, propertyName: String? = nil) throws -> Any? { + if let customizable = value as? WrapCustomizable { + return try self.performCustomWrapping(object: customizable) + } + + if let date = value as? WrappableDate { + return self.wrap(date: date) + } + + let mirror = Mirror(reflecting: value) + + if mirror.children.isEmpty { + if let displayStyle = mirror.displayStyle { + switch displayStyle { + case .enum: + if let wrappableEnum = value as? WrappableEnum { + if let wrapped = wrappableEnum.wrap(context: self.context, dateFormatter: self.dateFormatter) { + return wrapped + } + + throw WrapError.wrappingFailedForObject(value) + } + + return "\(value)" + case .struct: + return [:] + default: + return value + } + } + + if !(value is CustomStringConvertible) { + if String(describing: value) == "(Function)" { + return nil + } + } + + return value + } else if value is ExpressibleByNilLiteral && mirror.children.count == 1 { + if let firstMirrorChild = mirror.children.first { + return try self.wrap(value: firstMirrorChild.value, propertyName: propertyName) + } + } + + return try self.wrap(object: value, enableCustomizedWrapping: false) + } + + func wrap(collection: T) throws -> [Any] { + var wrappedArray = [Any]() + let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) + + for element in collection { + if let wrapped = try wrapper.wrap(value: element) { + wrappedArray.append(wrapped) + } + } + + return wrappedArray + } + + func wrap(dictionary: [K : V]) throws -> WrappedDictionary { + var wrappedDictionary = WrappedDictionary() + let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) + + for (key, value) in dictionary { + let wrappedKey: String? + + if let stringKey = key as? String { + wrappedKey = stringKey + } else if let wrappableKey = key as? WrappableKey { + wrappedKey = wrappableKey.toWrappedKey() + } else if let stringConvertible = key as? CustomStringConvertible { + wrappedKey = stringConvertible.description + } else { + wrappedKey = nil + } + + if let wrappedKey = wrappedKey { + wrappedDictionary[wrappedKey] = try wrapper.wrap(value: value, propertyName: wrappedKey) + } + } + + return wrappedDictionary + } + + func wrap(date: WrappableDate) -> String { + let dateFormatter: DateFormatter + + if let existingFormatter = self.dateFormatter { + dateFormatter = existingFormatter + } else { + dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + self.dateFormatter = dateFormatter + } + + return date.wrap(dateFormatter: dateFormatter) + } + + func performWrapping(object: T, mirrors: [Mirror]) throws -> WrappedDictionary { + let customizable = object as? WrapCustomizable + var wrappedDictionary = WrappedDictionary() + + for mirror in mirrors { + for property in mirror.children { + + if (property.value as? WrapOptional)?.isNil == true { + continue + } + + guard let propertyName = property.label else { + continue + } + + let wrappingKey: String? + + if let customizable = customizable { + wrappingKey = customizable.keyForWrapping(propertyNamed: propertyName) + } else { + wrappingKey = propertyName + } + + if let wrappingKey = wrappingKey { + if let wrappedProperty = try customizable?.wrap(propertyNamed: propertyName, originalValue: property.value, context: self.context, dateFormatter: self.dateFormatter) { + wrappedDictionary[wrappingKey] = wrappedProperty + } else { + wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) + } + } + } + } + + return wrappedDictionary + } + + func performCustomWrapping(object: WrapCustomizable) throws -> Any { + guard let wrapped = object.wrap(context: self.context, dateFormatter: self.dateFormatter) else { + throw WrapError.wrappingFailedForObject(object) + } + + return wrapped + } +} + +// MARK: - Nil Handling + +private protocol WrapOptional { + var isNil: Bool { get } +} + +extension Optional : WrapOptional { + var isNil: Bool { + switch self { + case .none: + return true + case .some(let wrapped): + if let nillable = wrapped as? WrapOptional { + return nillable.isNil + } + return false + } + } +} \ No newline at end of file diff --git a/Telegram/BUILD b/Telegram/BUILD index 56527eec83a..32cf4972292 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -250,16 +250,17 @@ filegroup( name = "AppStringResources", srcs = [ "Telegram-iOS/en.lproj/Localizable.strings", + "//Swiftgram/SGStrings:SGLocalizableStrings", ] + [ "{}.lproj/Localizable.strings".format(language) for language in empty_languages - ], + ] ) filegroup( name = "WatchAppStringResources", srcs = glob([ "Telegram-iOS/*.lproj/Localizable.strings", - ], exclude = ["Telegram-iOS/*.lproj/**/.*"]), + ], exclude = ["Telegram-iOS/*.lproj/**/.*"]) + ["//Swiftgram/SGStrings:SGLocalizableStrings"], ) filegroup( @@ -318,19 +319,21 @@ filegroup( ]), ) +# MARK: Swiftgram alternative icons alternate_icon_folders = [ - "BlackIcon", - "BlackClassicIcon", - "BlackFilledIcon", - "BlueIcon", - "BlueClassicIcon", - "BlueFilledIcon", - "WhiteFilledIcon", - "New1", - "New2", - "Premium", - "PremiumBlack", - "PremiumTurbo", + "SGDefault", + "SGBlack", + "SGLegacy", + "SGInverted", + "SGWhite", + "SGNight", + "SGSky", + "SGTitanium", + "SGNeon", + "SGNeonBlue", + "SGGlass", + "SGSparkling", + "SGBeta" ] [ @@ -356,12 +359,14 @@ objc_library( ], ) +SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets"] + swift_library( name = "Lib", srcs = glob([ "Telegram-iOS/Application.swift", ]), - data = [ + data = SGRESOURCES + [ ":Icons", ":AppResources", ":AppIntentVocabularyResources", @@ -422,6 +427,16 @@ plist_fragment( tonsite + + CFBundleTypeRole + Viewer + CFBundleURLName + {telegram_bundle_id}.custom + CFBundleURLSchemes + + sg + + """.format( telegram_bundle_id = telegram_bundle_id, @@ -508,6 +523,7 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id applinks:telegram.me applinks:t.me applinks:*.t.me + applinks:swiftgram.app """ @@ -537,7 +553,7 @@ official_communication_notifications_fragment = """ com.apple.developer.usernotifications.communication """ -communication_notifications_fragment = official_communication_notifications_fragment if telegram_bundle_id in official_bundle_ids else "" +communication_notifications_fragment = official_communication_notifications_fragment # if telegram_bundle_id in official_bundle_ids else "" store_signin_fragment = """ com.apple.developer.applesignin @@ -547,6 +563,13 @@ store_signin_fragment = """ """ signin_fragment = store_signin_fragment if telegram_bundle_id in store_bundle_ids else "" +# content_analysis = """ +# com.apple.developer.sensitivecontentanalysis.client +# +# analysis +# +# """ + plist_fragment( name = "TelegramEntitlements", extension = "entitlements", @@ -561,6 +584,7 @@ plist_fragment( carplay_fragment, communication_notifications_fragment, signin_fragment, + # content_analysis ]) ) @@ -648,7 +672,7 @@ plist_fragment( template = """ CFBundleDisplayName - Telegram + Swiftgram """ ) @@ -696,7 +720,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.watchkitapp.watchkitextension CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! WKExtensionDelegateClassName @@ -716,7 +740,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.watchkitapp CFBundleName - Telegram + Swiftgram UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -1160,7 +1184,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Share CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1252,7 +1276,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationContent CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1359,7 +1383,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Widget CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1472,7 +1496,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.SiriIntents CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1593,6 +1617,147 @@ ios_extension( ], ) +# MARK: Swiftgram +# TODO(swiftgram): Localize CFBundleDisplayName +plist_fragment( + name = "SGActionRequestHandlerInfoPlist", + extension = "plist", + template = + """ + CFBundleDevelopmentRegion + en + CFBundleIdentifier + {telegram_bundle_id}.SGActionRequestHandler + CFBundleName + Swiftgram + CFBundleDisplayName + Open in Swiftgram + CFBundlePackageType + XPC! + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 0 + NSExtensionActivationSupportsImageWithMaxCount + 0 + NSExtensionActivationSupportsMovieWithMaxCount + 0 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + NSExtensionJavaScriptPreprocessingFile + Action + NSExtensionServiceAllowsFinderPreviewItem + + NSExtensionServiceAllowsTouchBarItem + + NSExtensionServiceFinderPreviewIconName + NSActionTemplate + NSExtensionServiceTouchBarBezelColorName + TouchBarBezel + NSExtensionServiceTouchBarIconName + NSActionTemplate + + NSExtensionPointIdentifier + com.apple.services + NSExtensionPrincipalClass + SGActionRequestHandler + + """.format( + telegram_bundle_id = telegram_bundle_id, + ) +) + +# TODO(swiftgram): Proper icon +filegroup( + name = "SGActionRequestHandlerAssets", + srcs = glob(["SGActionRequestHandler/Media.xcassets/**"]), + visibility = ["//visibility:public"], +) + +filegroup( + name = "SGActionRequestHandlerScript", + srcs = ["SGActionRequestHandler/Action.js"], + visibility = ["//visibility:public"], +) + +swift_library( + name = "SGActionRequestHandlerLib", + module_name = "SGActionRequestHandlerLib", + srcs = glob([ + "SGActionRequestHandler/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + data = [ + ":SGActionRequestHandlerAssets", + ":SGActionRequestHandlerScript" + ], + deps = [ + "//submodules/UrlEscaping:UrlEscaping" + ], +) + +genrule( + name = "SetMinOsVersionSGActionRequestHandler", + cmd_bash = +""" + name=SGActionRequestHandler.appex + cat $(location PatchMinOSVersion.source.sh) | sed -e "s/<<>>/14\\.0/g" | sed -e "s/<<>>/$$name/g" > $(location SetMinOsVersionSGActionRequestHandler.sh) +""", + srcs = [ + "PatchMinOSVersion.source.sh", + ], + outs = [ + "SetMinOsVersionSGActionRequestHandler.sh", + ], + executable = True, + visibility = [ + "//visibility:public", + ] +) + +ios_extension( + name = "SGActionRequestHandler", + bundle_id = "{telegram_bundle_id}.SGActionRequestHandler".format( + telegram_bundle_id = telegram_bundle_id, + ), + families = [ + "iphone", + "ipad", + ], + infoplists = [ + ":SGActionRequestHandlerInfoPlist", + ":VersionInfoPlist", + ":RequiredDeviceCapabilitiesPlist", + ":BuildNumberInfoPlist", + # ":AppNameInfoPlist", + ], + minimum_os_version = minimum_os_version, # maintain the same minimum OS version across extensions + ipa_post_processor = ":SetMinOsVersionSGActionRequestHandler", + #provides_main = True, + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "@build_configuration//provisioning:SGActionRequestHandler.mobileprovision", + }), + deps = [ + ":SGActionRequestHandlerLib", + ], + frameworks = [ + ], + visibility = [ + "//visibility:public", + ] +) +# + plist_fragment( name = "BroadcastUploadInfoPlist", extension = "plist", @@ -1603,7 +1768,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.BroadcastUpload CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1697,7 +1862,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationService CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1764,11 +1929,11 @@ plist_fragment( CFBundleDevelopmentRegion en CFBundleDisplayName - Telegram + Swiftgram CFBundleIdentifier {telegram_bundle_id} CFBundleName - Telegram + Swiftgram CFBundlePackageType APPL CFBundleSignature @@ -1821,17 +1986,17 @@ plist_fragment( NSCameraUsageDescription We need this so that you can take and share photos and videos. NSContactsUsageDescription - Telegram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices. + Swiftgram stores your contacts heavily encrypted in the Telegram cloud to let you connect with your friends across all your devices. NSFaceIDUsageDescription You can use Face ID to unlock the app. NSLocationAlwaysUsageDescription - When you send your location to your friends, Telegram needs access to show them a map. You also need this to send locations from an Apple Watch. + When you send your location to your friends, Swiftgram needs access to show them a map. You also need this to send locations from an Apple Watch. NSLocationWhenInUseUsageDescription - When you send your location to your friends, Telegram needs access to show them a map. + When you send your location to your friends, Swiftgram needs access to show them a map. NSMicrophoneUsageDescription We need this so that you can record and share voice messages and videos with sound. NSMotionUsageDescription - When you send your location to your friends, Telegram needs access to show them a map. + When you send your location to your friends, Swiftgram needs access to show them a map. NSPhotoLibraryAddUsageDescription We need this so that you can share photos and videos from your photo library. NSPhotoLibraryUsageDescription @@ -1938,7 +2103,7 @@ xcode_provisioning_profile( ) ios_application( - name = "Telegram", + name = "Swiftgram", bundle_id = "{telegram_bundle_id}".format( telegram_bundle_id = telegram_bundle_id, ), @@ -1975,9 +2140,12 @@ ios_application( strings = [ ":AppStringResources", ], + # MARK: Swiftgram + settings_bundle = "//Swiftgram/SGSettingsBundle:SGSettingsBundle", extensions = select({ ":disableExtensionsSetting": [], "//conditions:default": [ + # ":SGActionRequestHandler", # UX sucks https://t.me/swiftgramchat/7335 ":ShareExtension", ":NotificationContentExtension", ":NotificationServiceExtension" + notificationServiceExtensionVersion, @@ -2001,11 +2169,11 @@ xcodeproj( name = "Telegram_xcodeproj", build_mode = "bazel", bazel_path = telegram_bazel_path, - project_name = "Telegram", + project_name = "Swiftgram", tags = ["manual"], top_level_targets = top_level_targets( labels = [ - ":Telegram", + ":Swiftgram", ], target_environments = ["device", "simulator"], ), diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 74adf27ecc5..d79d2a58a11 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -495,14 +495,16 @@ private struct NotificationContent: CustomStringConvertible { var userInfo: [AnyHashable: Any] = [:] var attachments: [UNNotificationAttachment] = [] var silent = false + var isEmpty: Bool var senderPerson: INPerson? var senderImage: INImage? var isLockedMessage: String? - init(isLockedMessage: String?) { + init(isLockedMessage: String?, isEmpty: Bool = false) { self.isLockedMessage = isLockedMessage + self.isEmpty = isEmpty } var description: String { @@ -518,6 +520,7 @@ private struct NotificationContent: CustomStringConvertible { string += " senderImage: \(self.senderImage != nil ? "non-empty" : "empty"),\n" string += " isLockedMessage: \(String(describing: self.isLockedMessage)),\n" string += " attachments: \(self.attachments),\n" + string += " isEmpty: \(self.isEmpty),\n" string += "}" return string } @@ -635,6 +638,16 @@ private struct NotificationContent: CustomStringConvertible { } } } + + // MARK: Swiftgram + if self.isEmpty { + content.title = " " + content.threadIdentifier = "empty-notification" + if #available(iOSApplicationExtension 15.0, iOS 15.0, *) { + content.interruptionLevel = .passive + content.relevanceScore = 0.0 + } + } return content } @@ -998,7 +1011,7 @@ private final class NotificationServiceHandler { action = .logout case "MESSAGE_MUTED": if let peerId = peerId { - action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil) + action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil, isEmpty: true), messageId: nil) } case "MESSAGE_DELETED": if let peerId = peerId { @@ -1241,7 +1254,7 @@ private final class NotificationServiceHandler { case .logout: Logger.shared.log("NotificationService \(episode)", "Will logout") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(isLockedMessage: nil, isEmpty: true) updateCurrentContent(content) completed() case let .poll(peerId, initialContent, messageId): @@ -1667,7 +1680,7 @@ private final class NotificationServiceHandler { queue.async { guard let strongSelf = self, let stateManager = strongSelf.stateManager else { - let content = NotificationContent(isLockedMessage: isLockedMessage) + let content = NotificationContent(isLockedMessage: isLockedMessage, isEmpty: true) updateCurrentContent(content) completed() return @@ -1953,7 +1966,7 @@ private final class NotificationServiceHandler { postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(isLockedMessage: nil, isEmpty: true) if isCurrentAccount { content.badge = Int(value.0) } @@ -1995,7 +2008,7 @@ private final class NotificationServiceHandler { } let completeRemoval: () -> Void = { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(isLockedMessage: nil, isEmpty: true) Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") updateCurrentContent(content) @@ -2047,7 +2060,7 @@ private final class NotificationServiceHandler { postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(isLockedMessage: nil, isEmpty: true) if isCurrentAccount { content.badge = Int(value.0) } @@ -2088,7 +2101,7 @@ private final class NotificationServiceHandler { } let completeRemoval: () -> Void = { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(isLockedMessage: nil, isEmpty: true) updateCurrentContent(content) completed() @@ -2141,11 +2154,64 @@ final class NotificationService: UNNotificationServiceExtension { private let content = Atomic(value: nil) private var contentHandler: ((UNNotificationContent) -> Void)? private var episode: String? + // MARK: Swiftgram + private var emptyNotificationsRemoved: Bool = false + private var notificationRemovalTries: Int32 = 0 + private let maxNotificationRemovalTries: Int32 = 30 override init() { super.init() } + // MARK: Swiftgram + func removeEmptyNotificationsOnce() { + var emptyNotifications: [String] = [] + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in + for notification in notifications { + if notification.request.content.threadIdentifier == "empty-notification" { + emptyNotifications.append(notification.request.identifier) + } + } + if !emptyNotifications.isEmpty { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: emptyNotifications) + #if DEBUG + NSLog("Empty notifications removed once. Count \(emptyNotifications.count)") + #endif + } + }) + } + + func removeEmptyNotifications() { + self.notificationRemovalTries += 1 + if self.emptyNotificationsRemoved || self.notificationRemovalTries > self.maxNotificationRemovalTries { + #if DEBUG + NSLog("Notification removal try rejected \(self.notificationRemovalTries)") + #endif + return + } + var emptyNotifications: [String] = [] + #if DEBUG + NSLog("Notification removal try \(notificationRemovalTries)") + #endif + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in + for notification in notifications { + if notification.request.content.threadIdentifier == "empty-notification" { + emptyNotifications.append(notification.request.identifier) + } + } + if !emptyNotifications.isEmpty { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: emptyNotifications) + self.emptyNotificationsRemoved = true + #if DEBUG + NSLog("Empty notifications removed on try \(self.notificationRemovalTries). Count \(emptyNotifications.count)") + #endif + } else { + self.removeEmptyNotifications() + } + }) + + } + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { let episode = String(UInt32.random(in: 0 ..< UInt32.max), radix: 16) self.episode = episode @@ -2176,7 +2242,12 @@ final class NotificationService: UNNotificationServiceExtension { strongSelf.contentHandler = nil if let content = content.with({ $0 }) { + // MARK: Swiftgram + strongSelf.removeEmptyNotificationsOnce() contentHandler(content.generate()) + if content.isEmpty { + strongSelf.removeEmptyNotifications() + } } else if let initialContent = strongSelf.initialContent { contentHandler(initialContent) } diff --git a/Telegram/SGActionRequestHandler/Action.js b/Telegram/SGActionRequestHandler/Action.js new file mode 100644 index 00000000000..11832ae69cf --- /dev/null +++ b/Telegram/SGActionRequestHandler/Action.js @@ -0,0 +1,21 @@ +var Action = function() {}; + +Action.prototype = { + run: function(arguments) { + var payload = { + "url": document.documentURI + } + arguments.completionFunction(payload) + }, + finalize: function(arguments) { + const alertMessage = arguments["alert"] + const openURL = arguments["openURL"] + if (alertMessage) { + alert(alertMessage) + } else if (openURL) { + window.location = openURL + } + } +}; + +var ExtensionPreprocessingJS = new Action diff --git a/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json b/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json b/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json new file mode 100644 index 00000000000..94a9fc21819 --- /dev/null +++ b/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "mac", + "color" : { + "reference" : "systemPurpleColor" + } + } + ] +} \ No newline at end of file diff --git a/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift b/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift new file mode 100644 index 00000000000..31ccdff0215 --- /dev/null +++ b/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift @@ -0,0 +1,62 @@ +// import UIKit +// import MobileCoreServices +// import UrlEscaping + +// @objc(SGActionRequestHandler) +// class SGActionRequestHandler: NSObject, NSExtensionRequestHandling { +// var extensionContext: NSExtensionContext? + +// func beginRequest(with context: NSExtensionContext) { +// // Do not call super in an Action extension with no user interface +// self.extensionContext = context + +// let itemProvider = context.inputItems +// .compactMap({ $0 as? NSExtensionItem }) +// .reduce([NSItemProvider](), { partialResult, acc in +// var nextResult = partialResult +// nextResult += acc.attachments ?? [] +// return nextResult +// }) +// .filter({ $0.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) }) +// .first + +// guard let itemProvider = itemProvider else { +// return doneWithInvalidLink() +// } + +// itemProvider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { [weak self] item, error in +// DispatchQueue.main.async { +// guard +// let dictionary = item as? NSDictionary, +// let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary +// else { +// self?.doneWithInvalidLink() +// return +// } + +// if let url = results["url"] as? String, let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) { +// self?.doneWithResults(["openURL": "sg://parseurl?url=\(escapedUrl)"]) +// } else { +// self?.doneWithInvalidLink() +// } +// } +// }) +// } + +// func doneWithInvalidLink() { +// doneWithResults(["alert": "Invalid link"]) +// } + +// func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { +// if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { +// let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] +// let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: kUTTypePropertyList as String) +// let resultsItem = NSExtensionItem() +// resultsItem.attachments = [resultsProvider] +// self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) +// } else { +// self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) +// } +// self.extensionContext = nil +// } +// } diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json deleted file mode 100644 index 3364b2ef961..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon4@40x40-2.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon4@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon4@58x58-2.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon4@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon4@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon4@120x120-1.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon4@120x120.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon4@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon4@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon4@40x40.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon4@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon4@58x58.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon4@40x40-1.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon4@80x80.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon4@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon4@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon4@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png deleted file mode 100644 index 7169c854c35..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png deleted file mode 100644 index 7169c854c35..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png deleted file mode 100644 index 1529bf21a23..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png deleted file mode 100644 index 90e1de1ecfd..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png deleted file mode 100644 index d905a092330..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png deleted file mode 100644 index f7ed065d316..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png deleted file mode 100644 index 20070867ec3..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png deleted file mode 100644 index 74aaa26f789..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png deleted file mode 100644 index 74aaa26f789..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png deleted file mode 100644 index 8d3559c8ffb..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png deleted file mode 100644 index 63146351553..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png deleted file mode 100644 index 2948e25763c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png deleted file mode 100644 index 2948e25763c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png deleted file mode 100644 index 5176f7d26c9..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json deleted file mode 100644 index 8497b5a0d59..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon2@40x40.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon2@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon2@58x58.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon2@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon2@80x80.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon2@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon2@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon2@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon2@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon2@40x40-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon2@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon2@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon2@40x40-2.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon2@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon2@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon2@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon2@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png deleted file mode 100644 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png deleted file mode 100644 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png deleted file mode 100644 index 8044873c25c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png deleted file mode 100644 index bd9821af487..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png deleted file mode 100644 index a1d6016afb5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png deleted file mode 100644 index 090c237445f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png deleted file mode 100644 index 58f01e4c423..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png deleted file mode 100644 index e311513f498..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png deleted file mode 100644 index e311513f498..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png deleted file mode 100644 index d7e2100fda9..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png deleted file mode 100644 index fb36db9ebaa..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png deleted file mode 100644 index b327187568f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png deleted file mode 100644 index b327187568f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png deleted file mode 100644 index 7a1aec12714..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json deleted file mode 100644 index 021eed91bff..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon3@40x40.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon3@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon3@58x58.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon3@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon3@80x80.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon3@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon3@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon3@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon3@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon3@40x40-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon3@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon3@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon3@40x40-2.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon3@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon3@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon3@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon3@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png deleted file mode 100644 index 9c5ca6a0cf8..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png deleted file mode 100644 index 9c5ca6a0cf8..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png deleted file mode 100644 index de9fce9981d..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png deleted file mode 100644 index fb761143f01..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png deleted file mode 100644 index a09fd70b81d..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png deleted file mode 100644 index d5409e8ef14..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png deleted file mode 100644 index f9cf8bf6950..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png deleted file mode 100644 index 8b5050f6238..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png deleted file mode 100644 index 8b5050f6238..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png deleted file mode 100644 index dfea84b1b2c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png deleted file mode 100644 index dfba84e32f0..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png deleted file mode 100644 index 3f1f9d34ee5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png deleted file mode 100644 index 3f1f9d34ee5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png deleted file mode 100644 index 3c350f16499..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json deleted file mode 100644 index 5315597fb05..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon1@40x40-2.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon1@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon1@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon1@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon1@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon1@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon1@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon1@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon1@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon1@40x40.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon1@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon1@58x58.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon1@40x40-1.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon1@80x80.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon1@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon1@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon1@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png deleted file mode 100644 index d71dcd205e7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png deleted file mode 100644 index f51ae17df90..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png deleted file mode 100644 index facbf49ff3a..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png deleted file mode 100644 index e865e6256b4..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png deleted file mode 100644 index 4865bb8b078..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png deleted file mode 100644 index ffae9ee7b74..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png deleted file mode 100644 index 07de560340f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png deleted file mode 100644 index 95b278c284f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json deleted file mode 100644 index da4a164c918..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png deleted file mode 100755 index 093f5821a54..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png deleted file mode 100755 index 13f8fe26949..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png deleted file mode 100755 index 46593ec4658..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png deleted file mode 100755 index ed0216f9318..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png deleted file mode 100755 index 1fcc6fc9bbf..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png deleted file mode 100644 index 20fe7d5eefc..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png deleted file mode 100644 index fafc0e385e6..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png deleted file mode 100644 index f00e3e2d617..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png deleted file mode 100755 index a327546043e..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png deleted file mode 100755 index a3972adecaa..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png deleted file mode 100644 index d86fb7cb550..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png deleted file mode 100755 index 0b52118b1c0..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png deleted file mode 100644 index 90e1de1ecfd..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png deleted file mode 100755 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png deleted file mode 100755 index a1d6016afb5..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png deleted file mode 100755 index fb36db9ebaa..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png deleted file mode 100755 index 8044873c25c..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png deleted file mode 100755 index bd9821af487..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png deleted file mode 100755 index 55ae148ed83..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png deleted file mode 100755 index 638b30f339a..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png deleted file mode 100755 index 8b28ed057bc..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png deleted file mode 100755 index aa3ec282ce5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png deleted file mode 100755 index eca037efcf9..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png deleted file mode 100755 index 2e5e919205f..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png deleted file mode 100755 index 08da0b799af..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png deleted file mode 100755 index 342e2766d98..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png deleted file mode 100644 index b8befb1c7b3..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png deleted file mode 100644 index aa84350de54..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png deleted file mode 100644 index 32683874aa5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png deleted file mode 100755 index 7c851299aa5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png deleted file mode 100755 index 49b7fd968c7..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png deleted file mode 100644 index dfba84e32f0..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png deleted file mode 100644 index de9fce9981d..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png deleted file mode 100644 index fb761143f01..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png deleted file mode 100755 index 2e502e7dab1..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png deleted file mode 100755 index c47aeed4b14..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png deleted file mode 100755 index f07ad9568b3..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png deleted file mode 100755 index 1b21e8d9280..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png deleted file mode 100755 index 9bf363744d5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png deleted file mode 100755 index dc5916282e1..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png deleted file mode 100755 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png deleted file mode 100755 index f7725e9914c..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png deleted file mode 100644 index dd360d8f50d..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png deleted file mode 100644 index 2e502e7dab1..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png deleted file mode 100644 index c47aeed4b14..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png deleted file mode 100644 index 6d9e7ab98c7..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png deleted file mode 100644 index 9bf363744d5..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png deleted file mode 100644 index dc5916282e1..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png deleted file mode 100644 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png deleted file mode 100644 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png deleted file mode 100644 index f7725e9914c..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json index 4d65457087b..b45cfedbdcd 100644 --- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json +++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json @@ -1,110 +1,9 @@ { "images" : [ { - "filename" : "BlueNotificationIcon@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "BlueNotificationIcon@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "Simple@58x58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Simple@87x87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "Simple@80x80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "BlueIcon@2x-1.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "BlueIcon@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "BlueIcon@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "BlueNotificationIcon.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "BlueNotificationIcon@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "Simple@29x29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "Simple@58x58-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Simple@40x40-1.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "Simple@80x80-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "BlueIconIpad@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "BlueIconLargeIpad@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "Simple-iTunesArtwork.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "Swiftgram.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png deleted file mode 100644 index f00a2857f0f..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png deleted file mode 100644 index 90d7b67bc03..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png deleted file mode 100644 index a79cb5dcdc0..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png deleted file mode 100644 index aa6a4a442ec..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png deleted file mode 100644 index aa6a4a442ec..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png deleted file mode 100644 index 385bc474b20..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png deleted file mode 100644 index 385bc474b20..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png deleted file mode 100644 index c0a9ce93195..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png new file mode 100644 index 00000000000..a28a393d1ec Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json index da4a164c918..73c00596a7f 100644 --- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json +++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram/Telegram-iOS/IconDefault-60@2x.png b/Telegram/Telegram-iOS/IconDefault-60@2x.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-60@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-60@3x.png b/Telegram/Telegram-iOS/IconDefault-60@3x.png deleted file mode 100644 index facbf49ff3a..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-60@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-76.png b/Telegram/Telegram-iOS/IconDefault-76.png deleted file mode 100644 index 07de560340f..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-76@2x.png b/Telegram/Telegram-iOS/IconDefault-76@2x.png deleted file mode 100644 index d71dcd205e7..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-83.5@2x.png b/Telegram/Telegram-iOS/IconDefault-83.5@2x.png deleted file mode 100644 index f51ae17df90..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40.png b/Telegram/Telegram-iOS/IconDefault-Small-40.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png b/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png b/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small.png b/Telegram/Telegram-iOS/IconDefault-Small.png deleted file mode 100644 index 4865bb8b078..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small@2x.png b/Telegram/Telegram-iOS/IconDefault-Small@2x.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small@3x.png b/Telegram/Telegram-iOS/IconDefault-Small@3x.png deleted file mode 100644 index 95b278c284f..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-76.png b/Telegram/Telegram-iOS/New1.alticon/New1-76.png deleted file mode 100644 index c85f9bc45ae..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png deleted file mode 100644 index 32adc011d1a..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png deleted file mode 100644 index 93238e0c7f4..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1@2x.png deleted file mode 100644 index 70ddc32cbec..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1@3x.png b/Telegram/Telegram-iOS/New1.alticon/New1@3x.png deleted file mode 100644 index ced492fd402..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png b/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png deleted file mode 100644 index 6387cb01bde..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png b/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png deleted file mode 100644 index 93c34d10c79..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png b/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png deleted file mode 100644 index fb4f4a61220..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png b/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png deleted file mode 100644 index 1b3e74dfa60..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification.png deleted file mode 100644 index 34afc4fbec0..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png deleted file mode 100644 index e29005ac4e2..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png deleted file mode 100644 index 54a04f2c27f..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-76.png b/Telegram/Telegram-iOS/New2.alticon/New2-76.png deleted file mode 100644 index ca043ed3397..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png deleted file mode 100644 index d94dc86c61f..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png deleted file mode 100644 index 813a39a5bdb..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png deleted file mode 100644 index fe2d70eada0..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png deleted file mode 100644 index 6750299df3f..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small.png deleted file mode 100644 index 4e0265fa69a..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png deleted file mode 100644 index 37e2e15d18a..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png deleted file mode 100644 index 18156810473..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2@2x.png deleted file mode 100644 index 85de9e3fab6..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2@3x.png deleted file mode 100644 index a083c562d44..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2_notification.png b/Telegram/Telegram-iOS/New2.alticon/New2_notification.png deleted file mode 100644 index d2d9d52e92c..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2_notification.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png deleted file mode 100644 index caab8e9d8af..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png b/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png deleted file mode 100644 index 00ea76d714b..00000000000 Binary files a/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png b/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png deleted file mode 100644 index 1a67519593f..00000000000 Binary files a/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png b/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png deleted file mode 100644 index cb953d3546c..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png b/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png deleted file mode 100644 index a3833ef0c77..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png b/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png deleted file mode 100644 index 7eccb509eef..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png b/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png deleted file mode 100644 index a243d72e636..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png b/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png index 7a5a342bc9c..7260909f913 100644 Binary files a/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png and b/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png differ diff --git a/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png b/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png index 826d68b263e..5bb5b80fc8f 100644 Binary files a/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png and b/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png new file mode 100644 index 00000000000..c5c51e0b713 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png new file mode 100644 index 00000000000..c2c05b41922 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png differ diff --git a/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png new file mode 100644 index 00000000000..9ece7f08aa2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png new file mode 100644 index 00000000000..532041e0c37 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png differ diff --git a/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@2x.png b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@2x.png new file mode 100644 index 00000000000..02c8cbb05c6 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@2x.png differ diff --git a/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png new file mode 100644 index 00000000000..6485f589180 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png differ diff --git a/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@2x.png b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@2x.png new file mode 100644 index 00000000000..a70a819abdd Binary files /dev/null and b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@2x.png differ diff --git a/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png new file mode 100644 index 00000000000..43a38972b7f Binary files /dev/null and b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png differ diff --git a/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@2x.png b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@2x.png new file mode 100644 index 00000000000..f8131ff5175 Binary files /dev/null and b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@2x.png differ diff --git a/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png new file mode 100644 index 00000000000..e1fc51be8fa Binary files /dev/null and b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png differ diff --git a/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png new file mode 100644 index 00000000000..bc4426140ff Binary files /dev/null and b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png differ diff --git a/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png new file mode 100644 index 00000000000..f6e25e84cde Binary files /dev/null and b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png new file mode 100644 index 00000000000..1e6dd6862e4 Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png new file mode 100644 index 00000000000..ff12511d04b Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png new file mode 100644 index 00000000000..191c60f764f Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png new file mode 100644 index 00000000000..cc37ad00f2f Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png new file mode 100644 index 00000000000..df54b8cd97a Binary files /dev/null and b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png new file mode 100644 index 00000000000..2c238b101a7 Binary files /dev/null and b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png differ diff --git a/Telegram/Telegram-iOS/SGSky.alticon/SGSky@2x.png b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@2x.png new file mode 100644 index 00000000000..94d7ec24a09 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@2x.png differ diff --git a/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png new file mode 100644 index 00000000000..d4ac553a2d2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png differ diff --git a/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png new file mode 100644 index 00000000000..5cadd1d556b Binary files /dev/null and b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png differ diff --git a/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png new file mode 100644 index 00000000000..c5bfeb8f1e2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png differ diff --git a/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png new file mode 100644 index 00000000000..e6a99cf622b Binary files /dev/null and b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png differ diff --git a/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png new file mode 100644 index 00000000000..2e2b2bc6c32 Binary files /dev/null and b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png differ diff --git a/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png new file mode 100644 index 00000000000..3f9a26322da Binary files /dev/null and b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png differ diff --git a/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png new file mode 100644 index 00000000000..b6070b75dec Binary files /dev/null and b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png differ diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png deleted file mode 100644 index 2e52591bc34..00000000000 Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png deleted file mode 100644 index fb138b52286..00000000000 Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist index 0a71b7adbae..fcdf65836c9 100644 --- a/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - أرسل رسالة لخالد عبر تيليجرام (Telegram) وأخبره أن هديته وصلت إلى المنزل + أرسل رسالة لخالد عبر تيليجرام (Swiftgram) وأخبره أن هديته وصلت إلى المنزل diff --git a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings index 174276a9040..f52a6d14f9a 100644 --- a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings +++ b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings @@ -1,5 +1,5 @@ /* Localized versions of Info.plist keys */ -"CFBundleDisplayName" = "تيليجرام"; + "NSContactsUsageDescription" = "سيقوم تيليجرام برفع جهات الاتصال الخاصة بك باستمرار إلى خوادم التخزين السحابية ذات التشفير العالي لتتمكن من التواصل مع أصدقائك من خلال جميع أجهزتك."; "NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، تيليجرام يحتاج لصلاحيات لعرض الخريطة لهم."; "NSLocationAlwaysAndWhenInUseUsageDescription" = "عندما تختار أن تشارك مكانك بشكل حي مع أصدقائك في المحادثة، يحتاج تيليجرام إلى الوصول لموقعك في الخلفية حتى بعد إغلاق تيليجرام خلال فترة المشاركة."; diff --git a/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist index 39561210606..25217fc93b4 100644 --- a/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Sende Lisa eine Telegram-Nachricht, dass ich in 15 Minuten da bin. + Sende Lisa eine Swiftgram-Nachricht, dass ich in 15 Minuten da bin. diff --git a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings index ca338663408..9f47a12484e 100644 --- a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings +++ b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings @@ -1,9 +1,9 @@ /* Localized versions of Info.plist keys */ -"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; -"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; -"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; -"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; +"NSContactsUsageDescription" = "Swiftgram will continuously upload your contacts to Telegram's heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Swiftgram needs access to show them a map."; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing."; +"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; "NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls."; "NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library."; "NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library."; diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 68fc11e9fbd..69cff500db9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -395,23 +395,23 @@ "Date.ChatDateHeaderYear" = "%1$@ %2$@, %3$@"; // Tour -"Tour.Title1" = "Telegram"; +"Tour.Title1" = "Swiftgram"; "Tour.Text1" = "The world's **fastest** messaging app.\nIt is **free** and **secure**."; "Tour.Title2" = "Fast"; -"Tour.Text2" = "**Telegram** delivers messages\nfaster than any other application."; +"Tour.Text2" = "**Swiftgram** delivers messages\nfaster than any other application."; "Tour.Title3" = "Powerful"; -"Tour.Text3" = "**Telegram** has no limits on\nthe size of your media and chats."; +"Tour.Text3" = "**Swiftgram** has no limits on\nthe size of your media and chats."; "Tour.Title4" = "Secure"; -"Tour.Text4" = "**Telegram** keeps your messages\nsafe from hacker attacks."; +"Tour.Text4" = "**Swiftgram** keeps your messages\nsafe from hacker attacks."; "Tour.Title5" = "Cloud-Based"; -"Tour.Text5" = "**Telegram** lets you access your\nmessages from multiple devices."; +"Tour.Text5" = "**Swiftgram** lets you access your\nmessages from multiple devices."; "Tour.Title6" = "Free"; -"Tour.Text6" = "**Telegram** provides free unlimited\ncloud storage for chats and media."; +"Tour.Text6" = "**Swiftgram** provides free unlimited\ncloud storage for chats and media."; "Tour.StartButton" = "Start Messaging"; @@ -426,7 +426,7 @@ "Login.CallRequestState3" = "Telegram dialed your number\n[Didn't get the code?]"; "Login.EmailNotConfiguredError" = "An email account is required so that you can send us details about the error.\n\nPlease go to your device‘s settings > Passwords & Accounts > Add account and set up an email account."; "Login.EmailCodeSubject" = "%@, no code"; -"Login.EmailCodeBody" = "My phone number is:\n%@\nI can't get an activation code for Telegram."; +"Login.EmailCodeBody" = "My phone number is:\n%@\nI can't get an activation code for Swiftgram."; "Login.UnknownError" = "An error occurred, please try again later."; "Login.InvalidCodeError" = "Invalid code, please try again."; "Login.NetworkError" = "Please check your internet connection and try again."; @@ -437,13 +437,13 @@ "Login.InvalidLastNameError" = "Sorry, this last name can't be used."; "Login.InvalidPhoneEmailSubject" = "Invalid phone number: %@"; -"Login.InvalidPhoneEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram says it's invalid. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@"; +"Login.InvalidPhoneEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Swiftgram says it's invalid. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@"; "Login.PhoneBannedEmailSubject" = "Banned phone number: %@"; -"Login.PhoneBannedEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram says it's banned. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@"; +"Login.PhoneBannedEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Swiftgram says it's banned. Please help.\n\nApp version: %2$@\nOS version: %3$@\nLocale: %4$@\nMNC: %5$@"; -"Login.PhoneGenericEmailSubject" = "Telegram iOS error: %@"; -"Login.PhoneGenericEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Telegram shows an error. Please help.\n\nError: %2$@\nApp version: %3$@\nOS version: %4$@\nLocale: %5$@\nMNC: %6$@"; +"Login.PhoneGenericEmailSubject" = "Swiftgram iOS error: %@"; +"Login.PhoneGenericEmailBody" = "I'm trying to use my mobile phone number: %1$@\nBut Swiftgram shows an error. Please help.\n\nError: %2$@\nApp version: %3$@\nOS version: %4$@\nLocale: %5$@\nMNC: %6$@"; "Login.PhoneTitle" = "Your Phone"; @@ -499,8 +499,8 @@ "Contacts.Title" = "Contacts"; "Contacts.FailedToSendInvitesMessage" = "An error occurred."; "Contacts.AccessDeniedError" = "Telegram does not have access to your contacts"; -"Contacts.AccessDeniedHelpLandscape" = "Please go to your %@ Settings — Privacy — Contacts.\nThen select ON for Telegram."; -"Contacts.AccessDeniedHelpPortrait" = "Please go to your %@ Settings — Privacy — Contacts. Then select ON for Telegram."; +"Contacts.AccessDeniedHelpLandscape" = "Please go to your %@ Settings — Privacy — Contacts.\nThen select ON for Swiftgram."; +"Contacts.AccessDeniedHelpPortrait" = "Please go to your %@ Settings — Privacy — Contacts. Then select ON for Swiftgram."; "Contacts.AccessDeniedHelpON" = "ON"; "Contacts.InviteToTelegram" = "Invite to Telegram"; "Contacts.InviteFriends" = "Invite Friends"; @@ -538,7 +538,7 @@ "Conversation.Contact" = "Contact"; "Conversation.BlockUser" = "Block User"; "Conversation.UnblockUser" = "Unblock User"; -"Conversation.UnsupportedMedia" = "This message is not supported on your version of Telegram. Update the app to view:\nhttps://telegram.org/update"; +"Conversation.UnsupportedMedia" = "This message is not supported on your version of Swiftgram. Update the app to view:\nhttps://apps.apple.com/app/id6471879502"; "Conversation.EncryptionWaiting" = "Waiting for %@ to get online..."; "Conversation.EncryptionProcessing" = "Exchanging encryption keys..."; "Conversation.EmptyPlaceholder" = "No messages here yet..."; @@ -842,9 +842,9 @@ "BroadcastListInfo.AddRecipient" = "Add Recipient"; "Settings.LogoutConfirmationTitle" = "Log out?"; -"Settings.LogoutConfirmationText" = "\nNote that you can seamlessly use Telegram on all your devices at once.\n\nRemember, logging out kills all your Secret Chats."; +"Settings.LogoutConfirmationText" = "\nNote that you can seamlessly use Swiftgram/Telegram on all your devices at once.\n\nRemember, logging out kills all your Secret Chats."; -"Login.PadPhoneHelp" = "\nYou can use your main mobile number to log in to Telegram on all devices.\nDon't use your iPad's SIM number here — we'll need to send you an SMS.\n\nIs this number correct?\n{number}"; +"Login.PadPhoneHelp" = "\nYou can use your main mobile number to log in to Swiftgram on all devices.\nDon't use your iPad's SIM number here — we'll need to send you an SMS.\n\nIs this number correct?\n{number}"; "Login.PadPhoneHelpTitle" = "Your Number"; "MessageTimer.Custom" = "Custom"; @@ -1212,7 +1212,7 @@ "SharedMedia.EmptyFilesText" = "You can send and receive\nfiles of any type up to 1.5 GB each\nand access them anywhere."; "ShareFileTip.Title" = "Sharing Files"; -"ShareFileTip.Text" = "You can share **uncompressed** media files from your Camera Roll here.\n\nTo share files of any other type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram."; +"ShareFileTip.Text" = "You can share **uncompressed** media files from your Camera Roll here.\n\nTo share files of any other type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Swiftgram."; "ShareFileTip.CloseTip" = "Close Tip"; "DialogList.SearchSectionDialogs" = "Chats and Contacts"; @@ -1270,32 +1270,32 @@ "AccessDenied.Title" = "Please Allow Access"; -"AccessDenied.Contacts" = "Telegram messaging is based on your existing contact list.\n\nPlease go to Settings > Privacy > Contacts and set Telegram to ON."; +"AccessDenied.Contacts" = "Swiftgram messaging is based on your existing contact list.\n\nPlease go to Settings > Privacy > Contacts and set Swiftgram to ON."; -"AccessDenied.VoiceMicrophone" = "Telegram needs access to your microphone to send voice messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; +"AccessDenied.VoiceMicrophone" = "Swiftgram needs access to your microphone to send voice messages.\n\nPlease go to Settings > Privacy > Microphone and set Swiftgram to ON."; -"AccessDenied.VideoMicrophone" = "Telegram needs access to your microphone to record sound in videos recording.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; +"AccessDenied.VideoMicrophone" = "Swiftgram needs access to your microphone to record sound in videos recording.\n\nPlease go to Settings > Privacy > Microphone and set Swiftgram to ON."; -"AccessDenied.MicrophoneRestricted" = "Microphone access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Microphone and set Telegram to ON."; +"AccessDenied.MicrophoneRestricted" = "Microphone access is restricted for Swiftgram.\n\nPlease go to Settings > General > Restrictions > Microphone and set Swiftgram to ON."; -"AccessDenied.Camera" = "Telegram needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON."; +"AccessDenied.Camera" = "Swiftgram needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set Swiftgram to ON."; -"AccessDenied.CameraRestricted" = "Camera access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Camera and set Telegram to ON."; +"AccessDenied.CameraRestricted" = "Camera access is restricted for Swiftgram.\n\nPlease go to Settings > General > Restrictions > Camera and set Swiftgram to ON."; "AccessDenied.CameraDisabled" = "Camera access is globally restricted on your phone.\n\nPlease go to Settings > General > Restrictions and set Camera to ON"; -"AccessDenied.PhotosAndVideos" = "Telegram needs access to your photo library to send photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON."; +"AccessDenied.PhotosAndVideos" = "Swiftgram needs access to your photo library to send photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Swiftgram to ON."; -"AccessDenied.SaveMedia" = "Telegram needs access to your photo library to save photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON."; +"AccessDenied.SaveMedia" = "Swiftgram needs access to your photo library to save photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Swiftgram to ON."; -"AccessDenied.PhotosRestricted" = "Photo access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Photos and set Telegram to ON."; +"AccessDenied.PhotosRestricted" = "Photo access is restricted for Swiftgram.\n\nPlease go to Settings > General > Restrictions > Photos and set Swiftgram to ON."; -"AccessDenied.LocationDenied" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; +"AccessDenied.LocationDenied" = "Swiftgram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set Swiftgram to ON."; -"AccessDenied.LocationDisabled" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; +"AccessDenied.LocationDisabled" = "Swiftgram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; -"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; +"AccessDenied.LocationTracking" = "Swiftgram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; "AccessDenied.Settings" = "Settings"; @@ -1433,7 +1433,7 @@ "Conversation.FileDropbox" = "Dropbox"; "Conversation.FileOpenIn" = "Open in..."; -"Conversation.FileHowToText" = "To share files of any type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram."; +"Conversation.FileHowToText" = "To share files of any type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Swiftgram."; "Map.LocationTitle" = "Location"; "Map.OpenInMaps" = "Open in Maps"; @@ -1469,7 +1469,7 @@ "ChangePhone.ErrorOccupied" = "The number %@ is already connected to a Telegram account. Please delete that account before migrating to the new number."; -"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; +"AccessDenied.LocationTracking" = "Swiftgram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; "PrivacySettings.AuthSessions" = "Active Sessions"; "AuthSessions.Title" = "Active Sessions"; @@ -1579,7 +1579,7 @@ "Login.PhoneNumberHelp" = "Help"; "Login.EmailPhoneSubject" = "Invalid number %@"; -"Login.EmailPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's invalid. Please help.\nAdditional Info: %@, %@."; +"Login.EmailPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Swiftgram says it's invalid. Please help.\nAdditional Info: %@, %@."; "SharedMedia.TitleLink" = "Shared Links"; "SharedMedia.EmptyLinksText" = "All links shared in this chat will appear here."; @@ -1876,7 +1876,7 @@ "Cache.ClearProgress" = "Please Wait..."; "Cache.ClearEmpty" = "Empty"; "Cache.ByPeerHeader" = "CHATS"; -"Cache.Indexing" = "Telegram is calculating current cache size.\nThis can take a few minutes."; +"Cache.Indexing" = "Swiftgram is calculating current cache size.\nThis can take a few minutes."; "ExplicitContent.AlertTitle" = "Sorry"; "ExplicitContent.AlertChannel" = "You can't access this channel because it violates App Store rules."; @@ -2291,11 +2291,11 @@ Unused sets are archived when you add more."; "Conversation.JumpToDate" = "Jump To Date"; "Conversation.AddToReadingList" = "Add to Reading List"; -"AccessDenied.CallMicrophone" = "Telegram needs access to your microphone for voice calls.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; +"AccessDenied.CallMicrophone" = "Swiftgram needs access to your microphone for voice calls.\n\nPlease go to Settings > Privacy > Microphone and set Swiftgram to ON."; "Call.EncryptionKey.Title" = "Encryption Key"; -"Application.Name" = "Telegram"; +"Application.Name" = "Swiftgram"; "DialogList.Pin" = "Pin"; "DialogList.Unpin" = "Unpin"; "DialogList.PinLimitError" = "Sorry, you can pin no more than %@ chats to the top."; @@ -2345,7 +2345,7 @@ Unused sets are archived when you add more."; "Calls.AddTab" = "Add Tab"; "Calls.NewCall" = "New Call"; -"Calls.RatingTitle" = "Please rate the quality\nof your Telegram call"; +"Calls.RatingTitle" = "Please rate the quality\nof your Swiftgram call"; "Calls.SubmitRating" = "Submit"; "Call.Seconds_1" = "%@ second"; @@ -2532,14 +2532,14 @@ Unused sets are archived when you add more."; "Calls.RatingFeedback" = "Write a comment..."; -"Call.StatusIncoming" = "Telegram Audio..."; +"Call.StatusIncoming" = "Swiftgram Audio..."; "Call.IncomingVoiceCall" = "Incoming Voice Call"; "Call.IncomingVideoCall" = "Incoming Video Call"; "Call.StatusRequesting" = "Contacting..."; "Call.StatusWaiting" = "Waiting..."; "Call.StatusRinging" = "Ringing..."; "Call.StatusConnecting" = "Connecting..."; -"Call.StatusOngoing" = "Telegram Audio %@"; +"Call.StatusOngoing" = "Swiftgram Audio %@"; "Call.StatusEnded" = "Call Ended"; "Call.StatusFailed" = "Call Failed"; "Call.StatusBusy" = "Busy"; @@ -2604,7 +2604,7 @@ Unused sets are archived when you add more."; "Call.AudioRouteHeadphones" = "Headphones"; "Call.AudioRouteHide" = "Hide"; -"Call.PhoneCallInProgressMessage" = "You can’t place a Telegram call if you’re already on a phone call."; +"Call.PhoneCallInProgressMessage" = "You can’t place a Swiftgram call if you’re already on a phone call."; "Call.RecordingDisabledMessage" = "Please end your call before recording a voice message."; "Call.EmojiDescription" = "If these emoji are the same on %@'s screen, this call is 100%% secure."; @@ -2614,8 +2614,8 @@ Unused sets are archived when you add more."; "Conversation.HoldForAudio" = "Hold to record audio. Tap to switch to video."; "Conversation.HoldForVideo" = "Hold to record video. Tap to switch to audio."; -"UserInfo.TelegramCall" = "Telegram Call"; -"UserInfo.TelegramVideoCall" = "Telegram Video Call"; +"UserInfo.TelegramCall" = "Swiftgram Call"; +"UserInfo.TelegramVideoCall" = "Swiftgram Video Call"; "UserInfo.PhoneCall" = "Phone Call"; "SharedMedia.CategoryMedia" = "Media"; @@ -2623,8 +2623,8 @@ Unused sets are archived when you add more."; "SharedMedia.CategoryLinks" = "Links"; "SharedMedia.CategoryOther" = "Audio"; -"AccessDenied.VideoMessageCamera" = "Telegram needs access to your camera to send video messages.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON."; -"AccessDenied.VideoMessageMicrophone" = "Telegram needs access to your microphone to send video messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; +"AccessDenied.VideoMessageCamera" = "Swiftgram needs access to your camera to send video messages.\n\nPlease go to Settings > Privacy > Camera and set Swiftgram to ON."; +"AccessDenied.VideoMessageMicrophone" = "Swiftgram needs access to your microphone to send video messages.\n\nPlease go to Settings > Privacy > Microphone and set Swiftgram to ON."; "ChatSettings.AutomaticVideoMessageDownload" = "AUTOMATIC VIDEO MESSAGE DOWNLOAD"; @@ -2650,7 +2650,7 @@ Unused sets are archived when you add more."; "Privacy.PaymentsTitle" = "PAYMENTS"; "Privacy.PaymentsClearInfo" = "Clear payment & shipping info"; -"Privacy.PaymentsClearInfoHelp" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data."; +"Privacy.PaymentsClearInfoHelp" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Swiftgram never stores your credit card data."; "Privacy.PaymentsClear.PaymentInfo" = "Payment Info"; "Privacy.PaymentsClear.ShippingInfo" = "Shipping Info"; @@ -2801,7 +2801,7 @@ Unused sets are archived when you add more."; "Contacts.PhoneNumber" = "Phone Number"; "Contacts.AddPhoneNumber" = "Add %@"; -"Contacts.ShareTelegram" = "Share Telegram"; +"Contacts.ShareTelegram" = "Share Swiftgram"; "Conversation.ViewChannel" = "VIEW CHANNEL"; "Conversation.ViewGroup" = "VIEW GROUP"; @@ -2868,7 +2868,7 @@ Unused sets are archived when you add more."; "Privacy.Calls.P2PHelp" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but will slightly decrease audio quality."; "Privacy.Calls.Integration" = "iOS Call Integration"; -"Privacy.Calls.IntegrationHelp" = "iOS Call Integration shows Telegram calls on the lock screen and in the system's call history. If iCloud sync is enabled, your call history is shared with Apple."; +"Privacy.Calls.IntegrationHelp" = "iOS Call Integration shows Swiftgram calls on the lock screen and in the system's call history. If iCloud sync is enabled, your call history is shared with Apple."; "Call.ReportPlaceholder" = "What went wrong?"; "Call.ReportIncludeLog" = "Send technical information"; @@ -2900,14 +2900,14 @@ Unused sets are archived when you add more."; "SocksProxySetup.UseForCalls" = "Use for calls"; "SocksProxySetup.UseForCallsHelp" = "Proxy servers may degrade the quality of your calls."; -"InviteText.URL" = "https://telegram.org/dl"; -"InviteText.SingleContact" = "Hey, I'm using Telegram to chat. Join me! Download it here: %@"; -"InviteText.ContactsCountText_1" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}"; -"InviteText.ContactsCountText_2" = "Hey, I'm using Telegram to chat – and so are 2 of our other contacts. Join us! Download it here: {url}"; -"InviteText.ContactsCountText_3_10" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; -"InviteText.ContactsCountText_any" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; -"InviteText.ContactsCountText_many" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; -"InviteText.ContactsCountText_0" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}"; +"InviteText.URL" = "https://apps.apple.com/app/id6471879502"; +"InviteText.SingleContact" = "Hey, I'm using Swiftgram to chat. Join me! Download it here: %@"; +"InviteText.ContactsCountText_1" = "Hey, I'm using Swiftgram to chat. Join me! Download it here: {url}"; +"InviteText.ContactsCountText_2" = "Hey, I'm using Swiftgram to chat – and so are 2 of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_3_10" = "Hey, I'm using Swiftgram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_any" = "Hey, I'm using Swiftgram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_many" = "Hey, I'm using Swiftgram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_0" = "Hey, I'm using Swiftgram to chat. Join me! Download it here: {url}"; "Invite.LargeRecipientsCountWarning" = "Please note that it may take some time for your device to send all of these invitations"; @@ -3063,12 +3063,12 @@ Unused sets are archived when you add more."; "NotificationSettings.ContactJoined" = "New Contacts"; -"AccessDenied.LocationAlwaysDenied" = "If you'd like to share your Live Location with friends, Telegram needs location access when the app is in the background.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to Always."; +"AccessDenied.LocationAlwaysDenied" = "If you'd like to share your Live Location with friends, Swiftgram needs location access when the app is in the background.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to Always."; "UserInfo.UnblockConfirmation" = "Unblock %@?"; "Login.BannedPhoneSubject" = "Banned phone number: %@"; -"Login.BannedPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's banned. Please help."; +"Login.BannedPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Swiftgram says it's banned. Please help."; "Conversation.StopLiveLocation" = "Stop Sharing"; @@ -3151,16 +3151,16 @@ Unused sets are archived when you add more."; "Privacy.PaymentsClearInfoDoneHelp" = "Payment & shipping info cleared."; -"InfoPlist.NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; -"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; +"InfoPlist.NSContactsUsageDescription" = "Swiftgram will continuously upload your contacts to Telegram's heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Swiftgram needs access to show them a map."; "InfoPlist.NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls."; "InfoPlist.NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library."; "InfoPlist.NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library."; "InfoPlist.NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound."; "InfoPlist.NSSiriUsageDescription" = "You can use Siri to send messages."; -"InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; -"InfoPlist.NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; -"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; +"InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing."; +"InfoPlist.NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; +"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Swiftgram needs access to show them a map."; "InfoPlist.NSFaceIDUsageDescription" = "You can use Face ID to unlock the app."; "Privacy.Calls.P2PNever" = "Never"; @@ -3304,7 +3304,7 @@ Unused sets are archived when you add more."; "DialogList.AdLabel" = "Proxy Sponsor"; "DialogList.AdNoticeAlert" = "The proxy you are using displays a sponsored channel in your chat list."; -"SocksProxySetup.AdNoticeHelp" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal your Telegram traffic."; +"SocksProxySetup.AdNoticeHelp" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal your Swiftgram traffic."; "SocksProxySetup.ShareProxyList" = "Share Proxy List"; @@ -3582,9 +3582,9 @@ Unused sets are archived when you add more."; "Passport.NotLoggedInMessage" = "Please log in to your account to use Telegram Passport"; -"Update.Title" = "Telegram Update"; -"Update.AppVersion" = "Telegram %@"; -"Update.UpdateApp" = "Update Telegram"; +"Update.Title" = "Swiftgram Update"; +"Update.AppVersion" = "Swiftgram %@"; +"Update.UpdateApp" = "Update Swiftgram"; "Update.Skip" = "Skip"; "ReportPeer.ReasonCopyright" = "Copyright"; @@ -3763,8 +3763,8 @@ Unused sets are archived when you add more."; "SocksProxySetup.PasteFromClipboard" = "Paste From Clipboard"; -"Share.AuthTitle" = "Log in to Telegram"; -"Share.AuthDescription" = "Open Telegram and log in to share."; +"Share.AuthTitle" = "Log in to Swiftgram"; +"Share.AuthDescription" = "Open Swiftgram and log in to share."; "Notifications.DisplayNamesOnLockScreen" = "Names on lock-screen"; "Notifications.DisplayNamesOnLockScreenInfoWithLink" = "Display names in notifications when the device is locked. To disable, make sure that \"Show Previews\" is also set to \"When Unlocked\" or \"Never\" in [iOS Settings]"; @@ -3851,7 +3851,7 @@ Unused sets are archived when you add more."; "InstantPage.TapToOpenLink" = "Tap to open the link:"; "InstantPage.RelatedArticleAuthorAndDateTitle" = "%1$@ • %2$@"; -"AuthCode.Alert" = "Your login code is %@. Enter it in the Telegram app where you are trying to log in.\n\nDo not give this code to anyone."; +"AuthCode.Alert" = "Your login code is %@. Enter it in the Swiftgram app where you are trying to log in.\n\nDo not give this code to anyone."; "Login.CheckOtherSessionMessages" = "Check your Telegram messages"; "Login.SendCodeViaSms" = "Get the code via SMS"; "Login.SendCodeViaCall" = "Call me to dictate the code"; @@ -3862,7 +3862,7 @@ Unused sets are archived when you add more."; "Login.CodeExpired" = "Code expired, please login again."; "Login.CancelSignUpConfirmation" = "Do you want to stop the registration process?"; -"Passcode.AppLockedAlert" = "Telegram\nLocked"; +"Passcode.AppLockedAlert" = "Swiftgram\nLocked"; "ChatList.ReadAll" = "Read All"; "ChatList.Read" = "Read"; @@ -3893,7 +3893,7 @@ Unused sets are archived when you add more."; "Permissions.NotificationsAllowInSettings.v0" = "Turn ON in Settings"; "Permissions.CellularDataTitle.v0" = "Enable Cellular Data"; -"Permissions.CellularDataText.v0" = "Don't worry, Telegram keeps network usage to a minimum. You can further control this in Settings > Data and Storage."; +"Permissions.CellularDataText.v0" = "Don't worry, Swiftgram keeps network usage to a minimum. You can further control this in Settings > Data and Storage."; "Permissions.CellularDataAllowInSettings.v0" = "Turn ON in Settings"; "Permissions.SiriTitle.v0" = "Turn ON Siri"; @@ -3904,7 +3904,7 @@ Unused sets are archived when you add more."; "Permissions.PrivacyPolicy" = "Privacy Policy"; "Contacts.PermissionsTitle" = "Access to Contacts"; -"Contacts.PermissionsText" = "Please allow Telegram access to your phonebook to seamlessly find all your friends."; +"Contacts.PermissionsText" = "Please allow Swiftgram access to your phonebook to seamlessly find all your friends."; "Contacts.PermissionsAllow" = "Allow Access"; "Contacts.PermissionsAllowInSettings" = "Allow in Settings"; "Contacts.PermissionsSuppressWarningTitle" = "Keep contacts disabled?"; @@ -3977,8 +3977,8 @@ Unused sets are archived when you add more."; "AttachmentMenu.WebSearch" = "Web Search"; -"Conversation.UnsupportedMediaPlaceholder" = "This message is not supported on your version of Telegram. Please update to the latest version."; -"Conversation.UpdateTelegram" = "UPDATE TELEGRAM"; +"Conversation.UnsupportedMediaPlaceholder" = "This message is not supported on your version of Swiftgram. Please update to the latest version."; +"Conversation.UpdateTelegram" = "UPDATE SWIFTGRAM"; "Cache.LowDiskSpaceText" = "Your phone has run out of available storage. Please free some space to download or upload media."; @@ -4115,7 +4115,7 @@ Unused sets are archived when you add more."; "Undo.DeletedChannel" = "Deleted channel"; "Undo.DeletedGroup" = "Deleted group"; -"AccessDenied.Wallpapers" = "Telegram needs access to your photo library to set a custom chat background.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON."; +"AccessDenied.Wallpapers" = "Swiftgram needs access to your photo library to set a custom chat background.\n\nPlease go to Settings > Privacy > Photos and set Swiftgram to ON."; "Conversation.ChatBackground" = "Chat Background"; "Conversation.ViewBackground" = "VIEW BACKGROUND"; @@ -4421,7 +4421,7 @@ Unused sets are archived when you add more."; "Undo.ChatDeletedForBothSides" = "Chat deleted for both sides"; -"AppUpgrade.Running" = "Optimizing Telegram... +"AppUpgrade.Running" = "Optimizing Swiftgram... This may take a while, depending on the size of the database. Please keep the app open until the process is finished. Sorry for the inconvenience."; @@ -5044,11 +5044,11 @@ Sorry for the inconvenience."; "Group.ErrorSupergroupConversionNotPossible" = "Sorry, you are a member of too many groups and channels. Please leave some before creating a new one."; "ClearCache.StorageTitle" = "%@ STORAGE"; -"ClearCache.StorageCache" = "Telegram Cache"; -"ClearCache.StorageServiceFiles" = "Telegram Service Files"; +"ClearCache.StorageCache" = "Swiftgram Cache"; +"ClearCache.StorageServiceFiles" = "Swiftgram Service Files"; "ClearCache.StorageOtherApps" = "Other Apps"; "ClearCache.StorageFree" = "Free"; -"ClearCache.ClearCache" = "Clear Telegram Cache"; +"ClearCache.ClearCache" = "Clear Swiftgram Cache"; "ClearCache.Clear" = "Clear"; "ClearCache.Forever" = "Forever"; @@ -5669,7 +5669,7 @@ Sorry for the inconvenience."; "Call.Audio" = "audio"; "Call.AudioRouteMute" = "Mute Yourself"; -"AccessDenied.VideoCallCamera" = "Telegram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON."; +"AccessDenied.VideoCallCamera" = "Swiftgram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set Swiftgram to ON."; "Call.AccountIsLoggedOnCurrentDevice" = "Sorry, you can't call %@ because that account is logged in to Telegram on the device you're using for the call."; @@ -5823,7 +5823,7 @@ Sorry for the inconvenience."; "Conversation.EditingPhotoPanelTitle" = "Edit Photo"; "Media.LimitedAccessTitle" = "Limited Access to Media"; -"Media.LimitedAccessText" = "You've given Telegram access only to select number of photos."; +"Media.LimitedAccessText" = "You've given Swiftgram access only to select number of photos."; "Media.LimitedAccessManage" = "Manage"; "Media.LimitedAccessSelectMore" = "Select More Photos..."; "Media.LimitedAccessChangeSettings" = "Change Settings"; @@ -6167,7 +6167,7 @@ Sorry for the inconvenience."; "Message.ImportedDateFormat" = "%1$@, %2$@ Imported %3$@"; "ChatImportActivity.Title" = "Importing Chat"; -"ChatImportActivity.OpenApp" = "Open Telegram"; +"ChatImportActivity.OpenApp" = "Open Swiftgram"; "ChatImportActivity.Retry" = "Retry"; "ChatImportActivity.InProgress" = "Please keep this window open\nuntil the import is completed."; "ChatImportActivity.ErrorNotAdmin" = "You need to be an admin in the group to import messages."; @@ -6319,7 +6319,7 @@ Sorry for the inconvenience."; "Widget.UpdatedAt" = "Updated {}"; "Intents.ErrorLockedTitle" = "Locked"; -"Intents.ErrorLockedText" = "Open Telegram and enter passcode to edit widget."; +"Intents.ErrorLockedText" = "Open Swiftgram and enter passcode to edit widget."; "Conversation.GigagroupDescription" = "Only admins can send messages in this group."; @@ -6625,7 +6625,7 @@ Sorry for the inconvenience."; "ScheduledIn.Years_any" = "%@ years"; "ScheduledIn.Months_many" = "%@ years"; -"Checkout.PaymentLiabilityAlert" = "Neither Telegram, nor {target} will have access to your credit card information. Credit card details will be handled only by the payment system, {payment_system}.\n\nPayments will go directly to the developer of {target}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank."; +"Checkout.PaymentLiabilityAlert" = "Neither Swiftgram/Telegram, nor {target} will have access to your credit card information. Credit card details will be handled only by the payment system, {payment_system}.\n\nPayments will go directly to the developer of {target}. Swiftgram/Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank."; "Checkout.OptionalTipItem" = "Tip (Optional)"; "Checkout.TipItem" = "Tip"; @@ -6926,7 +6926,7 @@ Sorry for the inconvenience."; "SponsoredMessageMenu.Info" = "What are sponsored\nmessages?"; "SponsoredMessageInfoScreen.Title" = "What are sponsored messages?"; -"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Telegram never uses your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together."; +"SponsoredMessageInfoScreen.MarkdownText" = "Unlike other apps, Swiftgram and Telegram never use your private data to target ads. [Learn more in the Privacy Policy](https://telegram.org/privacy#5-6-no-ads-based-on-user-data)\nYou are seeing this message only because someone chose this public one-to many channel as a space to promote their messages. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored message.\n\nUnline other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties can't spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible adverticers at:\n[url]\nAds should no longer be synonymous with abuse of user privacy. Let us redefine how a tech compony should operate — together."; "SponsoredMessageInfo.Action" = "Learn More"; "SponsoredMessageInfo.Url" = "https://telegram.org/ads"; @@ -7292,8 +7292,8 @@ Sorry for the inconvenience."; "Contacts.QrCode.MyCode" = "My QR Code"; "Contacts.QrCode.NoCodeFound" = "No valid QR code found in the image. Please try again."; -"AccessDenied.QrCode" = "Telegram needs access to your photo library to scan QR codes.\n\nOpen your device's Settings > Privacy > Photos and set Telegram to ON."; -"AccessDenied.QrCamera" = "Telegram needs access to your camera to scan QR codes.\n\nOpen your device's Settings > Privacy > Camera and set Telegram to ON."; +"AccessDenied.QrCode" = "Swiftgram needs access to your photo library to scan QR codes.\n\nOpen your device's Settings > Privacy > Photos and set Swiftgram to ON."; +"AccessDenied.QrCamera" = "Swiftgram needs access to your camera to scan QR codes.\n\nOpen your device's Settings > Privacy > Camera and set Swiftgram to ON."; "Share.ShareToInstagramStories" = "Share to Instagram Stories"; @@ -7406,8 +7406,8 @@ Sorry for the inconvenience."; "Attachment.MediaAccessText" = "Share an unlimited number of photos and videos of up to 2 GB each."; "Attachment.MediaAccessStoryText" = "Share an unlimited number of photos and videos of up to 2 GB each."; -"Attachment.LimitedMediaAccessText" = "You have limited Telegram from accessing all of your photos."; -"Attachment.CameraAccessText" = "Telegram needs camera access so that you can take photos and videos."; +"Attachment.LimitedMediaAccessText" = "You have limited Swiftgram from accessing all of your photos."; +"Attachment.CameraAccessText" = "Swiftgram needs camera access so that you can take photos and videos."; "Attachment.Manage" = "Manage"; "Attachment.OpenSettings" = "Go to Settings"; @@ -7451,8 +7451,8 @@ Sorry for the inconvenience."; "LiveStream.ViewerCount_any" = "%@ viewers"; "LiveStream.Watching" = "watching"; -"LiveStream.NoSignalAdminText" = "Oops! Telegram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app."; -"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Telegram."; +"LiveStream.NoSignalAdminText" = "Oops! Swiftgram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app."; +"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Swiftgram."; "LiveStream.ViewCredentials" = "View Stream Key"; @@ -7532,7 +7532,7 @@ Sorry for the inconvenience."; "WebApp.RemoveConfirmationText" = "Remove **%@** from the attachment menu?"; "Notifications.SystemTones" = "SYSTEM TONES"; -"Notifications.TelegramTones" = "TELEGRAM TONES"; +"Notifications.TelegramTones" = "SWIFTGRAM TONES"; "Notifications.UploadSound" = "Upload Sound"; "Notifications.MessageSoundInfo" = "Press and hold a short voice note or mp3 file in any chat and select \"Save for Notifications\". It will appear here."; @@ -7544,7 +7544,7 @@ Sorry for the inconvenience."; "Notifications.UploadError.TooLong.Title" = "%@ is too long."; "Notifications.UploadError.TooLong.Text" = "Duration must be less than %@."; "Notifications.UploadSuccess.Title" = "Sound Added"; -"Notifications.UploadSuccess.Text" = "The sound **%@** was added to your Telegram tones."; +"Notifications.UploadSuccess.Text" = "The sound **%@** was added to your Swiftgram tones."; "Notifications.SaveSuccess.Text" = "You can now use this sound as a notification tone in your [custom notification settings]()."; "Conversation.DeleteTimer.SetupTitle" = "Auto-Delete After..."; @@ -7632,7 +7632,7 @@ Sorry for the inconvenience."; "Premium.AppIcons.Proceed" = "Unlock Premium Icons"; "Premium.NoAds.Proceed" = "About Telegram Premium"; -"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On."; +"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Swiftgram and set Precise Location to On."; "Chat.MultipleTypingPair" = "%@ and %@"; "Chat.MultipleTypingMore" = "%@ and %@ others"; @@ -8055,7 +8055,7 @@ Sorry for the inconvenience."; "Login.Edit" = "Edit"; "Login.Yes" = "Yes"; -"Checkout.PaymentLiabilityBothAlert" = "Telegram will not have access to your credit card information. Credit card details will be handled only by the payment system, {target}.\n\nPayments will go directly to the developer of {target}. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank."; +"Checkout.PaymentLiabilityBothAlert" = "Swiftgram/Telegram will not have access to your credit card information. Credit card details will be handled only by the payment system, {target}.\n\nPayments will go directly to the developer of {target}. Swiftgram/Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of {target} or your bank."; "Settings.ChangeProfilePhoto" = "Change Profile Photo"; @@ -8615,7 +8615,7 @@ Sorry for the inconvenience."; "StorageManagement.DescriptionCleared" = "All media can be re-downloaded from the Telegram cloud if you need it again."; "StorageManagement.DescriptionChatUsage" = "This chat uses %1$@% of your Telegram cache."; -"StorageManagement.DescriptionAppUsage" = "Telegram uses %1$@% of your free disk space."; +"StorageManagement.DescriptionAppUsage" = "Swiftgram uses %1$@% of your free disk space."; "StorageManagement.ClearAll" = "Clear Entire Cache"; "StorageManagement.ClearSelected" = "Clear Selected"; @@ -9021,7 +9021,7 @@ Sorry for the inconvenience."; "PowerSavingScreen.OptionAutoplayEmojiText" = "Loop animated emoji in messages, reactions, statuses."; "PowerSavingScreen.OptionAutoplayEffectsTitle" = "Interface Effects"; -"PowerSavingScreen.OptionAutoplayEffectsText" = "Various effects and animations that make Telegram look amazing."; +"PowerSavingScreen.OptionAutoplayEffectsText" = "Various effects and animations that make Swiftgram look amazing."; "PowerSavingScreen.OptionBackgroundTitle" = "Extended Background Time"; "PowerSavingScreen.OptionBackgroundText" = "Update chats faster when switching between apps."; @@ -9382,7 +9382,7 @@ Sorry for the inconvenience."; "ChatList.PremiumRestoreDiscountTitle" = "Get Premium back with up to %@ off"; "ChatList.PremiumRestoreDiscountText" = "Your Telegram Premium has recently expired. Tap here to extend it."; -"Login.ErrorAppOutdated" = "Please update Telegram to the latest version to log in."; +"Login.ErrorAppOutdated" = "Please update Swiftgram to the latest version to log in."; "Login.GetCodeViaFragment" = "Get a code via Fragment"; @@ -9551,8 +9551,8 @@ Sorry for the inconvenience."; "Story.HeaderEdited" = "edited"; "Story.CaptionShowMore" = "Show more"; -"Story.UnsupportedText" = "This story is not supported by\nyour version of Telegram."; -"Story.UnsupportedAction" = "Update Telegram"; +"Story.UnsupportedText" = "This story is not supported by\nyour version of Swiftgram."; +"Story.UnsupportedAction" = "Update Swiftgram"; "Story.ScreenshotBlockedTitle" = "Screenshot Blocked"; "Story.ScreenshotBlockedText" = "The story you tried to take a\nscreenshot of is protected from\ncopying by its creator."; @@ -9627,7 +9627,7 @@ Sorry for the inconvenience."; "Story.Camera.SwipeLeftRelease" = "Release to lock"; "Story.Camera.SwipeRightToFlip" = "Swipe right to flip"; -"Story.Camera.AccessPlaceholderTitle" = "Allow Telegram to access your camera and microphone"; +"Story.Camera.AccessPlaceholderTitle" = "Allow Swiftgram to access your camera and microphone"; "Story.Camera.AccessPlaceholderText" = "This lets you share photos and record videos."; "Story.Camera.AccessOpenSettings" = "Open Settings"; @@ -9937,7 +9937,7 @@ Sorry for the inconvenience."; "Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once."; "WebApp.DisclaimerTitle" = "Terms of Use"; -"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."; +"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Swiftgram/Telegram. You must agree to the Terms of Use of mini apps to continue."; "WebApp.DisclaimerAgree" = "I agree to the [Terms of Use]()"; "WebApp.DisclaimerContinue" = "Continue"; "WebApp.Disclaimer_URL" = "https://telegram.org/tos/mini-apps"; diff --git a/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist index fd11102f14d..ae7044bbbcb 100644 --- a/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Envía un mensaje de Telegram a Alicia diciéndole que estarás allí en 15 minutos + Envía un mensaje de Swiftgram a Alicia diciéndole que estarás allí en 15 minutos diff --git a/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist index 8710a6c624b..550ebf81795 100644 --- a/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Invia un messaggio su Telegram (Telegramma) ad Alex dicendo che sarò lì tra 10 minuti + Invia un messaggio su Swiftgram (Swiftgramma) ad Alex dicendo che sarò lì tra 10 minuti diff --git a/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist index 932e5f6d28b..980b2b7448b 100644 --- a/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - 앨리스에게 나 15분 안에 도착한다고 Telegram 메시지 보내줘 + 앨리스에게 나 15분 안에 도착한다고 Swiftgram 메시지 보내줘 diff --git a/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist index 5c709b84ee3..fcc0a842f15 100644 --- a/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Stuur een Telegram-bericht naar Maartje met ik ben er over 15 minuten. + Stuur een Swiftgram-bericht naar Maartje met ik ben er over 15 minuten. diff --git a/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist index 018471dbc6f..2e33f8946e9 100644 --- a/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Enviar uma mensagem no Telegram para a Alice dizendo que eu chegarei lá em 15 minutos + Enviar uma mensagem no Swiftgram para a Alice dizendo que eu chegarei lá em 15 minutos diff --git a/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist index 72fd63d7384..643154cf8c5 100644 --- a/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Отправить Алисе сообщение в Telegram: я буду через 10 минут + Отправить Алисе сообщение в Swiftgram: я буду через 10 минут diff --git a/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/WORKSPACE b/WORKSPACE index 15eb608a33b..51cd9377307 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,4 +1,5 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository") http_archive( name = "bazel_features", @@ -87,3 +88,11 @@ load("@build_bazel_rules_apple//apple:apple.bzl", "provisioning_profile_reposito provisioning_profile_repository( name = "local_provisioning_profiles", ) + +# MARK: Swiftgram +new_git_repository( + name = "flex_sdk", + remote = "https://github.com/FLEXTool/FLEX.git", + commit = "1b983160cc188aff18284c1d990121cdb1e42e9c", + build_file = "@//Swiftgram/FLEX:FLEX.BUILD" +) \ No newline at end of file diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index 1052bb5ef1f..5b1ba1e05ff 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -9,6 +9,7 @@ class BuildConfiguration: def __init__(self, + sg_config, bundle_id, api_id, api_hash, @@ -22,6 +23,7 @@ def __init__(self, enable_siri, enable_icloud ): + self.sg_config = sg_config self.bundle_id = bundle_id self.api_id = api_id self.api_hash = api_hash @@ -39,6 +41,7 @@ def write_to_variables_file(self, bazel_path, use_xcode_managed_codesigning, aps string = '' string += 'telegram_bazel_path = "{}"\n'.format(bazel_path) string += 'telegram_use_xcode_managed_codesigning = {}\n'.format('True' if use_xcode_managed_codesigning else 'False') + string += 'sg_config = """{}"""\n'.format(self.sg_config) string += 'telegram_bundle_id = "{}"\n'.format(self.bundle_id) string += 'telegram_api_id = "{}"\n'.format(self.api_id) string += 'telegram_api_hash = "{}"\n'.format(self.api_hash) @@ -67,6 +70,7 @@ def build_configuration_from_json(path): with open(path) as file: configuration_dict = json.load(file) required_keys = [ + 'sg_config', 'bundle_id', 'api_id', 'api_hash', @@ -78,12 +82,13 @@ def build_configuration_from_json(path): 'app_specific_url_scheme', 'premium_iap_product_id', 'enable_siri', - 'enable_icloud' + 'enable_icloud', ] for key in required_keys: if key not in configuration_dict: print('Configuration at {} does not contain {}'.format(path, key)) return BuildConfiguration( + sg_config=configuration_dict['sg_config'], bundle_id=configuration_dict['bundle_id'], api_id=configuration_dict['api_id'], api_hash=configuration_dict['api_hash'], @@ -95,7 +100,7 @@ def build_configuration_from_json(path): app_specific_url_scheme=configuration_dict['app_specific_url_scheme'], premium_iap_product_id=configuration_dict['premium_iap_product_id'], enable_siri=configuration_dict['enable_siri'], - enable_icloud=configuration_dict['enable_icloud'] + enable_icloud=configuration_dict['enable_icloud'], ) @@ -115,6 +120,8 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base def load_codesigning_data_from_git(working_dir, repo_url, temp_key_path, branch, password, always_fetch): + # MARK: Swiftgram + branch = "master" if not os.path.exists(working_dir): os.makedirs(working_dir, exist_ok=True) @@ -155,6 +162,8 @@ def load_codesigning_data_from_git(working_dir, repo_url, temp_key_path, branch, def copy_profiles_from_directory(source_path, destination_path, team_id, bundle_id): profile_name_mapping = { + # Swiftgram + # '.SGActionRequestHandler': 'SGActionRequestHandler', '.SiriIntents': 'Intents', '.NotificationContent': 'NotificationContent', '.NotificationService': 'NotificationService', diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 971c8c2d80a..5cb36ab8814 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -327,7 +327,7 @@ def invoke_build(self): if self.custom_target is not None: combined_arguments += [self.custom_target] else: - combined_arguments += ['Telegram/Telegram'] + combined_arguments += ['Telegram/Swiftgram'] if self.continue_on_error: combined_arguments += ['--keep_going'] @@ -608,22 +608,22 @@ def build(bazel, arguments): if arguments.outputBuildArtifactsPath is not None: artifacts_path = os.path.abspath(arguments.outputBuildArtifactsPath) - if os.path.exists(artifacts_path + '/Telegram.ipa'): - os.remove(artifacts_path + '/Telegram.ipa') + if os.path.exists(artifacts_path + '/Swiftgram.ipa'): + os.remove(artifacts_path + '/Swiftgram.ipa') if os.path.exists(artifacts_path + '/DSYMs'): shutil.rmtree(artifacts_path + '/DSYMs') os.makedirs(artifacts_path, exist_ok=True) os.makedirs(artifacts_path + '/DSYMs', exist_ok=True) built_ipa_path_prefix = 'bazel-out/ios_arm64-opt-ios-arm64-min12.0-applebin_ios-ST-*' - ipa_paths = glob.glob('{}/bin/Telegram/Telegram.ipa'.format(built_ipa_path_prefix)) + ipa_paths = glob.glob('{}/bin/Telegram/Swiftgram.ipa'.format(built_ipa_path_prefix)) if len(ipa_paths) == 0: - print('Could not find the IPA at bazel-out/applebin_ios-ios_arm*-opt-ST-*/bin/Telegram/Telegram.ipa') + print('Could not find the IPA at bazel-out/applebin_ios-ios_arm*-opt-ST-*/bin/Telegram/Swiftgram.ipa') sys.exit(1) elif len(ipa_paths) > 1: print('Multiple matching IPA files found: {}'.format(ipa_paths)) sys.exit(1) - shutil.copyfile(ipa_paths[0], artifacts_path + '/Telegram.ipa') + shutil.copyfile(ipa_paths[0], artifacts_path + '/Swiftgram.ipa') dsym_paths = glob.glob('bazel-bin/Telegram/*.dSYM') for dsym_path in dsym_paths: @@ -633,7 +633,7 @@ def build(bazel, arguments): os.chdir(artifacts_path) run_executable_with_output('zip', arguments=[ '-r', - 'Telegram.DSYMs.zip', + 'Swiftgram.DSYMs.zip', './DSYMs' ], check_result=True) os.chdir(previous_directory) diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index a8759a89e21..953f4caa4cc 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -50,7 +50,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, call_executable(bazel_generate_arguments) - xcodeproj_path = '{}.xcodeproj'.format(app_target_spec.replace(':', '/')) + # MARK: Swiftgram + xcodeproj_path = 'Telegram/Swiftgram.xcodeproj' call_executable(['open', xcodeproj_path]) diff --git a/build-system/template_minimal_development_configuration.json b/build-system/template_minimal_development_configuration.json index 1aad0aed955..7c885537acd 100755 --- a/build-system/template_minimal_development_configuration.json +++ b/build-system/template_minimal_development_configuration.json @@ -1,5 +1,5 @@ { - "bundle_id": "org.{! a random string !}.Telegram", + "bundle_id": "org.{! a random string !}.Swiftgram", "api_id": "{! get one at https://my.telegram.org/apps !}", "api_hash": "{! get one at https://my.telegram.org/apps !}", "team_id": "{! check README.md !}", @@ -10,5 +10,6 @@ "app_specific_url_scheme": "tg", "premium_iap_product_id": "", "enable_siri": false, - "enable_icloud": false + "enable_icloud": false, + "sg_config": "" } \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..0ca4fe438e9 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,6 @@ +files: + - source: Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings + translation: /Swiftgram/SGStrings/Strings/%osx_code%/SGLocalizable.strings + translation_replace: + zh-Hans: zh-hans + zh-Hant: zh-hant diff --git a/submodules/AccountContext/BUILD b/submodules/AccountContext/BUILD index fb2ccfc448b..1a7b23dbbef 100644 --- a/submodules/AccountContext/BUILD +++ b/submodules/AccountContext/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "AccountContext", module_name = "AccountContext", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/TelegramAudio:TelegramAudio", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager", diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5e32fda5846..08d07518463 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -773,6 +774,8 @@ public protocol MediaEditorScreenResult { } public protocol TelegramRootControllerInterface: NavigationController { + var accountSettingsController: PeerInfoScreen? { get set } + @discardableResult func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index b2dd59e4048..477b04b4551 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -31,6 +31,8 @@ public final class ChatMessageItemAssociatedData: Equatable { } } + public let translateToLanguageSG: String? + public let translationSettings: TranslationSettings? public let automaticDownloadPeerType: MediaAutoDownloadPeerType public let automaticDownloadPeerId: EnginePeer.Id? public let automaticDownloadNetworkType: MediaAutoDownloadNetworkType @@ -66,6 +68,8 @@ public final class ChatMessageItemAssociatedData: Equatable { public let starGifts: [Int64: TelegramMediaFile] public init( + translateToLanguageSG: String? = nil, + translationSettings: TranslationSettings? = nil, automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, @@ -100,6 +104,8 @@ public final class ChatMessageItemAssociatedData: Equatable { showSensitiveContent: Bool = false, starGifts: [Int64: TelegramMediaFile] = [:] ) { + self.translateToLanguageSG = translateToLanguageSG + self.translationSettings = translationSettings self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId self.automaticDownloadNetworkType = automaticDownloadNetworkType @@ -139,6 +145,12 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.automaticDownloadPeerType != rhs.automaticDownloadPeerType { return false } + if lhs.translateToLanguageSG != rhs.translateToLanguageSG { + return false + } + if lhs.translationSettings != rhs.translationSettings { + return false + } if lhs.automaticDownloadPeerId != rhs.automaticDownloadPeerId { return false } @@ -962,6 +974,7 @@ public protocol PeerInfoScreen: ViewController { var privacySettings: Promise { get } func openBirthdaySetup() + func tabBarItemContextAction(sourceView: UIView, gesture: ContextGesture?) func toggleStorySelection(ids: [Int32], isSelected: Bool) func togglePaneIsReordering(isReordering: Bool) func cancelItemSelection() @@ -1012,6 +1025,7 @@ public protocol ChatControllerCustomNavigationPanelNode: ASDisplayNode { } public protocol ChatController: ViewController { + var overlayTitle: String? { get } var chatLocation: ChatLocation { get } var canReadHistory: ValuePromise { get } var parentController: ViewController? { get set } diff --git a/submodules/AccountContext/Sources/PeerNameColors.swift b/submodules/AccountContext/Sources/PeerNameColors.swift index 1966a168ea3..aec0fa51678 100644 --- a/submodules/AccountContext/Sources/PeerNameColors.swift +++ b/submodules/AccountContext/Sources/PeerNameColors.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import TelegramCore @@ -7,16 +8,16 @@ private extension PeerNameColors.Colors { if colors.colors.isEmpty { return nil } - self.main = UIColor(rgb: colors.colors[0]) + self._main = UIColor(rgb: colors.colors[0]) if colors.colors.count > 1 { - self.secondary = UIColor(rgb: colors.colors[1]) + self._secondary = UIColor(rgb: colors.colors[1]) } else { - self.secondary = nil + self._secondary = nil } if colors.colors.count > 2 { - self.tertiary = UIColor(rgb: colors.colors[2]) + self._tertiary = UIColor(rgb: colors.colors[2]) } else { - self.tertiary = nil + self._tertiary = nil } } } @@ -29,39 +30,67 @@ public class PeerNameColors: Equatable { } public struct Colors: Equatable { - public let main: UIColor - public let secondary: UIColor? - public let tertiary: UIColor? + private let _main: UIColor + private let _secondary: UIColor? + private let _tertiary: UIColor? + // MARK: Swiftgram + public var main: UIColor { + let currentSaturation = SGSimpleSettings.shared.accountColorsSaturation + if currentSaturation == 0 { + return _main + } else { + return _main.withReducedSaturation(CGFloat(currentSaturation) / 100.0) + } + } + + public var secondary: UIColor? { + let currentSaturation = SGSimpleSettings.shared.accountColorsSaturation + if currentSaturation == 0 { + return _secondary + } else { + return _secondary?.withReducedSaturation(CGFloat(currentSaturation) / 100.0) + } + } + + public var tertiary: UIColor? { + let currentSaturation = SGSimpleSettings.shared.accountColorsSaturation + if currentSaturation == 0 { + return _tertiary + } else { + return _tertiary?.withReducedSaturation(CGFloat(currentSaturation) / 100.0) + } + } public init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) { - self.main = main - self.secondary = secondary - self.tertiary = tertiary + self._main = main + self._secondary = secondary + self._tertiary = tertiary } public init(main: UIColor) { - self.main = main - self.secondary = nil - self.tertiary = nil + self._main = main + self._secondary = nil + self._tertiary = nil } public init?(colors: [UIColor]) { guard let first = colors.first else { return nil } - self.main = first + self._main = first if colors.count == 3 { - self.secondary = colors[1] - self.tertiary = colors[2] + self._secondary = colors[1] + self._tertiary = colors[2] } else if colors.count == 2, let second = colors.last { - self.secondary = second - self.tertiary = nil + self._secondary = second + self._tertiary = nil } else { - self.secondary = nil - self.tertiary = nil + self._secondary = nil + self._tertiary = nil } } } + public static var defaultSingleColors: [Int32: Colors] { return [ @@ -323,3 +352,20 @@ public class PeerNameColors: Equatable { return true } } + +// MARK: Swiftgram +extension UIColor { + func withReducedSaturation(_ factor: CGFloat) -> UIColor { + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + + if self.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { + let newSaturation = max(0, min(1, saturation * factor)) + return UIColor(hue: hue, saturation: newSaturation, brightness: brightness, alpha: alpha) + } + + return self + } +} \ No newline at end of file diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 0edf8c349cc..a418353b6fd 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -250,10 +250,10 @@ public struct PremiumConfiguration { isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? defaultValue.isPremiumDisabled, areStarsDisabled: data["stars_purchase_blocked"] as? Bool ?? defaultValue.areStarsDisabled, subscriptionManagementUrl: data["premium_manage_subscription_url"] as? String ?? "", - showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, - showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, - giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, - starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, + showPremiumGiftInAttachMenu: false, // data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, /* MARK: Swiftgram */ + showPremiumGiftInTextField: false, // data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, /* MARK: Swiftgram */ + giveawayGiftsPurchaseAvailable: false, // data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, /* MARK: Swiftgram */ + starsGiftsPurchaseAvailable: false, // data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, /* MARK: Swiftgram */ boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, diff --git a/submodules/AccountUtils/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift index 44d09f560fa..4ae60dbc163 100644 --- a/submodules/AccountUtils/Sources/AccountUtils.swift +++ b/submodules/AccountUtils/Sources/AccountUtils.swift @@ -4,8 +4,11 @@ import TelegramCore import TelegramUIPreferences import AccountContext -public let maximumNumberOfAccounts = 3 -public let maximumPremiumNumberOfAccounts = 4 +// MARK: Swiftgram +public let maximumSwiftgramNumberOfAccounts = 200 +public let maximumSafeNumberOfAccounts = 6 +public let maximumNumberOfAccounts = maximumSwiftgramNumberOfAccounts +public let maximumPremiumNumberOfAccounts = maximumSwiftgramNumberOfAccounts public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> { let sharedContext = context.sharedContext diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index b30194cc3a9..37bd9b62b8a 100644 --- a/submodules/AppLock/Sources/AppLock.swift +++ b/submodules/AppLock/Sources/AppLock.swift @@ -274,8 +274,8 @@ public final class AppLockContextImpl: AppLockContext { private func updateTimestampRenewTimer(shouldRun: Bool) { if shouldRun { - if self.timestampRenewTimer == nil { - let timestampRenewTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in + if self.timestampRenewTimer == nil { // MARK: Swiftgram + let timestampRenewTimer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in guard let strongSelf = self else { return } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 5344b52b730..631a911e2be 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -805,9 +805,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) - }, forwardSelectedMessages: { + }, forwardSelectedMessages: { _ in }, forwardCurrentForwardMessages: { - }, forwardMessages: { _ in + }, forwardMessages: { _, _ in }, updateForwardOptionsState: { [weak self] value in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState($0.forwardOptionsState) }) }) diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index f941572f4b8..9092e5d1681 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGStrings:SGStrings" +] + swift_library( name = "AuthorizationUI", module_name = "AuthorizationUI", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/TelegramCore:TelegramCore", "//submodules/Postbox:Postbox", diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index cb4520a32ff..fbcca5b064b 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -1,3 +1,5 @@ +import SGStrings + import Foundation import UIKit import AsyncDisplayKit @@ -591,11 +593,10 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if nextType == nil { if let controller { - let carrier = CTCarrier() - let mnc = carrier.mobileNetworkCode ?? "none" - let _ = strongSelf.engine.auth.reportMissingCode(phoneNumber: number, phoneCodeHash: phoneCodeHash, mnc: mnc).start() - - AuthorizationSequenceController.presentDidNotGetCodeUI(controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc) + // MARK: Swiftgram + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: i18n("Auth.UnofficialAppCodeTitle", strongSelf.presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: i18n("Common.OpenTelegram", strongSelf.presentationData.strings.baseLanguageCode), action: { + strongSelf.sharedContext.applicationBindings.openUrl("https://t.me/+42777") + })]), in: .window(.root)) } } else { controller?.inProgress = true diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index 74fd3996ef2..459be125dbe 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -276,6 +276,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})) self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) } else { + // MARK: Swiftgram + if (number == "0000000000") { + self.sharedContext.beginNewAuth(testingEnvironment: true) + return + } if let validLayout = self.validLayout, validLayout.size.width > 320.0 { let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index e32f74f78a2..9dcf81e51a2 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -583,7 +583,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } let contactSyncSize = self.contactSyncNode.updateLayout(width: maximumWidth) - if self.hasOtherAccounts { + if self.hasOtherAccounts || { return true }() { self.contactSyncNode.isHidden = false items.append(AuthorizationLayoutItem(node: self.contactSyncNode, size: contactSyncSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 14.0, maxValue: 14.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } else { diff --git a/submodules/BuildConfig/BUILD b/submodules/BuildConfig/BUILD index 7ac35f1be8a..aef09c3185f 100644 --- a/submodules/BuildConfig/BUILD +++ b/submodules/BuildConfig/BUILD @@ -1,5 +1,6 @@ load( "@build_configuration//:variables.bzl", + "sg_config", "telegram_api_id", "telegram_api_hash", "telegram_app_center_id", @@ -20,6 +21,7 @@ objc_library( ]), copts = [ "-Werror", + "-DAPP_SG_CONFIG=\\\"{}\\\"".format(sg_config.replace('"', '\\\\\\"')), "-DAPP_CONFIG_API_ID={}".format(telegram_api_id), "-DAPP_CONFIG_API_HASH=\\\"{}\\\"".format(telegram_api_hash), "-DAPP_CONFIG_APP_CENTER_ID=\\\"{}\\\"".format(telegram_app_center_id), diff --git a/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h b/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h index 9ff7cf8e7b9..1e098670747 100644 --- a/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h +++ b/submodules/BuildConfig/PublicHeaders/BuildConfig/BuildConfig.h @@ -12,6 +12,7 @@ - (instancetype _Nonnull)initWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId; @property (nonatomic, strong, readonly) NSString * _Nullable appCenterId; +@property (nonatomic, strong, readonly) NSString * _Nonnull sgConfig; @property (nonatomic, readonly) int32_t apiId; @property (nonatomic, strong, readonly) NSString * _Nonnull apiHash; @property (nonatomic, readonly) bool isInternalBuild; diff --git a/submodules/BuildConfig/Sources/BuildConfig.m b/submodules/BuildConfig/Sources/BuildConfig.m index a4f25b28d4e..6f51a025a58 100644 --- a/submodules/BuildConfig/Sources/BuildConfig.m +++ b/submodules/BuildConfig/Sources/BuildConfig.m @@ -70,6 +70,7 @@ - (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelle @interface BuildConfig () { NSData * _Nullable _bundleData; + NSString * _Nonnull _sgConfig; int32_t _apiId; NSString * _Nonnull _apiHash; NSString * _Nullable _appCenterId; @@ -127,6 +128,7 @@ + (instancetype _Nonnull)sharedBuildConfig { - (instancetype _Nonnull)initWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId { self = [super init]; if (self != nil) { + _sgConfig = @(APP_SG_CONFIG); _apiId = APP_CONFIG_API_ID; _apiHash = @(APP_CONFIG_API_HASH); _appCenterId = @(APP_CONFIG_APP_CENTER_ID); diff --git a/submodules/Camera/BUILD b/submodules/Camera/BUILD index a5150befbb3..90341cbdb30 100644 --- a/submodules/Camera/BUILD +++ b/submodules/Camera/BUILD @@ -8,6 +8,10 @@ load("//build-system/bazel-utils:plist_fragment.bzl", "plist_fragment", ) +sgDeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + filegroup( name = "CameraMetalResources", srcs = glob([ @@ -52,7 +56,7 @@ swift_library( data = [ ":CameraBundle", ], - deps = [ + deps = sgDeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 276feb37a36..59915bde16a 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -1,3 +1,5 @@ +import SGSimpleSettings + import Foundation import AVFoundation import UIKit @@ -338,6 +340,10 @@ final class CameraOutput: NSObject { AVVideoWidthKey: Int(dimensions.width), AVVideoHeightKey: Int(dimensions.height) ] + // MARK: Swiftgram + if SGSimpleSettings.shared.startTelescopeWithRearCam { + self.currentPosition = .back + } } else { let codecType: AVVideoCodecType = hasHEVCHardwareEncoder ? .hevc : .h264 if orientation == .landscapeLeft || orientation == .landscapeRight { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 67bc7a1af1e..2937fc1e407 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -1,15 +1,24 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGAPIWebSettings:SGAPIWebSettings", + "//Swiftgram/SGAPIToken:SGAPIToken" +] +sgsrcs = [ + "//Swiftgram/AppleStyleFolders:AppleStyleFolders" +] + swift_library( name = "ChatListUI", module_name = "ChatListUI", srcs = glob([ "Sources/**/*.swift", - ]), + ]) + sgsrcs, copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index b710deca991..dde62f944d4 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -363,7 +363,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) } - let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId + let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) /* && peerId == context.account.peerId // MARK: Swiftgram */ if let group = peerGroup { if archiveEnabled { let isArchived = group == .archive diff --git a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift index c28d06df04e..1e26beb2be6 100644 --- a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift +++ b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift @@ -68,7 +68,7 @@ final class ChatListContainerItemNode: ASDisplayNode { self.openArchiveSettings = openArchiveSettings self.isInlineMode = isInlineMode - self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady, isMainTab: isMainTab) + self.listNode = ChatListNode(getNavigationController: { return controller?.navigationController as? NavigationController }, context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady, isMainTab: isMainTab) if let controller, case .chatList(groupId: .root) = controller.location { self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6a6d3306d27..5a4590c1186 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1,3 +1,6 @@ +// MARK: Swiftgram +import SGSimpleSettings + import Foundation import UIKit import Postbox @@ -246,7 +249,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.tabsNode = SparseNode() - self.tabContainerNode = ChatListFilterTabContainerNode() + self.tabContainerNode = ChatListFilterTabContainerNode(context: context) self.tabsNode.addSubnode(self.tabContainerNode) super.init(context: context, navigationBarPresentationData: nil, mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource) @@ -375,12 +378,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.chatListDisplayNode.willScrollToTop() strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.scrollToPosition(.top(adjustForTempInset: false)) case let .known(offset): - let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == strongSelf.chatListDisplayNode.mainContainerNode.availableFilters.first?.filter + // MARK: Swiftgram + let sgAllChatsHiddden = SGSimpleSettings.shared.allChatsHidden + var mainContainerNode_availableFilters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters + if sgAllChatsHiddden { + mainContainerNode_availableFilters.removeAll { $0 == .all } + } + let isFirstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.currentItemNode.chatListFilter == mainContainerNode_availableFilters.first?.filter if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && strongSelf.chatListDisplayNode.inlineStackContainerNode != nil { strongSelf.setInlineChatList(location: nil) } else if offset <= ChatListNavigationBar.searchScrollHeight + 1.0 && !isFirstFilter { - let firstFilter = strongSelf.chatListDisplayNode.effectiveContainerNode.availableFilters.first ?? .all + // MARK: Swiftgram + var effectiveContainerNode_availableFilters = strongSelf.chatListDisplayNode.mainContainerNode.availableFilters + if sgAllChatsHiddden { + effectiveContainerNode_availableFilters.removeAll { $0 == .all } + } + let firstFilter = effectiveContainerNode_availableFilters.first ?? .all let targetTab: ChatListFilterTabEntryId switch firstFilter { case .all: @@ -738,8 +752,31 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if force { strongSelf.tabContainerNode.cancelAnimations() + // MARK: Swiftgram + strongSelf.chatListDisplayNode.inlineTabContainerNode.cancelAnimations() + strongSelf.chatListDisplayNode.appleStyleTabContainerNode.cancelAnimations() } strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: tabContainerData.2, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + // MARK: Swiftgram + strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0 * (SGSimpleSettings.shared.hideTabBar ? 3.0 : 1.0)), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: tabContainerData.2, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + strongSelf.chatListDisplayNode.appleStyleTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, /* canReorderAllChats: strongSelf.isPremium, filtersLimit: tabContainerData.2,*/ transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + + // MARK: Swiftgram + let switchingToFilterId: Int32 + switch (filter) { + case let .filter(filterId): + switchingToFilterId = filterId + default: + switchingToFilterId = -1 + } + + if fraction.isZero { + let accountId = "\(strongSelf.context.account.peerId.id._internalGetInt64Value())" + if SGSimpleSettings.shared.lastAccountFolders[accountId] != switchingToFilterId { + SGSimpleSettings.shared.lastAccountFolders[accountId] = switchingToFilterId + } + } + } self.reloadFilters() } @@ -929,6 +966,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let layout = self.validLayout { self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.effectiveContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) + self.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0 * (SGSimpleSettings.shared.hideTabBar ? 3.0 : 1.0)), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.effectiveContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) + self.chatListDisplayNode.appleStyleTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.effectiveContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, /*canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2,*/ transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) } if self.isNodeLoaded { @@ -1514,12 +1553,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.selectTab(id: id) } } - + self.chatListDisplayNode.inlineTabContainerNode.tabSelected = self.tabContainerNode.tabSelected + self.chatListDisplayNode.appleStyleTabContainerNode.tabSelected = self.tabContainerNode.tabSelected + self.tabContainerNode.tabRequestedDeletion = { [weak self] id in if case let .filter(id) = id { self?.askForFilterRemoval(id: id) } } + self.chatListDisplayNode.inlineTabContainerNode.tabRequestedDeletion = self.tabContainerNode.tabRequestedDeletion + self.chatListDisplayNode.appleStyleTabContainerNode.tabRequestedDeletion = self.tabContainerNode.tabRequestedDeletion self.tabContainerNode.presentPremiumTip = { [weak self] in if let strongSelf = self { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAll, customUndoText: strongSelf.presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in @@ -1538,6 +1581,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false }), in: .current) } } + self.chatListDisplayNode.inlineTabContainerNode.presentPremiumTip = self.tabContainerNode.presentPremiumTip + // self.chatListDisplayNode.appleStyleTabContainerNode.presentPremiumTip = self.tabContainerNode.presentPremiumTip let tabContextGesture: (Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool, Bool) -> Void = { [weak self] id, sourceNode, gesture, keepInPlace, isDisabled in guard let strongSelf = self else { @@ -1877,6 +1922,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.tabContainerNode.contextGesture = { id, sourceNode, gesture, isDisabled in tabContextGesture(id, sourceNode, gesture, false, isDisabled) } + self.chatListDisplayNode.inlineTabContainerNode.contextGesture = self.tabContainerNode.contextGesture + self.chatListDisplayNode.appleStyleTabContainerNode.contextGesture = self.tabContainerNode.contextGesture if case .chatList(.root) = self.location { self.ready.set(combineLatest([self.mainReady.get(), self.storiesReady.get()]) @@ -1979,11 +2026,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if self.previewing { self.storiesReady.set(.single(true)) } else { - self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive)) - |> deliverOnMainQueue).startStrict(next: { [weak self] rawStorySubscriptions in + // MARK: Swiftgram + let hideStoriesSignal = self.context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings]) + |> map { view -> Bool in + let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default + return settings.hideStories + } + |> distinctUntilChanged + + self.storySubscriptionsDisposable = (combineLatest(self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive)), hideStoriesSignal) + |> deliverOnMainQueue).startStrict(next: { [weak self] rawStorySubscriptions, hideStories in guard let self else { return } + var rawStorySubscriptions = rawStorySubscriptions + if hideStories { + rawStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil) + } self.rawStorySubscriptions = rawStorySubscriptions var items: [EngineStorySubscriptions.Item] = [] @@ -3274,6 +3333,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if !skipTabContainerUpdate { self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + // MARK: Swiftgram + self.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0 * (SGSimpleSettings.shared.hideTabBar ? 3.0 : 1.0)), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.chatListDisplayNode.appleStyleTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, /*canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2,*/ transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + } + let showFoldersAtBottom: Bool + if let tabContainerData = self.tabContainerData { + showFoldersAtBottom = tabContainerData.1 && tabContainerData.0.count > 1 + } else { + showFoldersAtBottom = false + } + self.tabContainerNode.isHidden = showFoldersAtBottom + + // MARK: Swiftgram + if showFoldersAtBottom { + let bottomFoldersStyle = SGSimpleSettings.shared.bottomTabStyle + self.chatListDisplayNode.inlineTabContainerNode.isHidden = SGSimpleSettings.BottomTabStyleValues.telegram.rawValue != bottomFoldersStyle + self.chatListDisplayNode.appleStyleTabContainerNode.isHidden = SGSimpleSettings.BottomTabStyleValues.ios.rawValue != bottomFoldersStyle + } else { + self.chatListDisplayNode.inlineTabContainerNode.isHidden = true + self.chatListDisplayNode.appleStyleTabContainerNode.isHidden = true } self.chatListDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: navigationBarHeight, storiesInset: 0.0, transition: transition) @@ -3318,6 +3397,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let layout = self.validLayout { self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) } + if SGSimpleSettings.shared.hideTabBar { + (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } @objc fileprivate func donePressed() { @@ -3344,6 +3426,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) } } + if SGSimpleSettings.shared.hideTabBar { + (self.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.2, curve: .easeInOut)) + } } private var skipTabContainerUpdate = false @@ -3362,7 +3447,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var reorderedFilterIdsValue: [Int32]? - if let reorderedFilterIds = self.tabContainerNode.reorderedFilterIds, reorderedFilterIds != defaultFilterIds { + // MARK: Swiftgram + let reorderedFilterIds: [Int32]? + if !self.chatListDisplayNode.inlineTabContainerNode.isHidden { + reorderedFilterIds = self.chatListDisplayNode.inlineTabContainerNode.reorderedFilterIds + } else if !self.chatListDisplayNode.appleStyleTabContainerNode.isHidden { + reorderedFilterIds = self.chatListDisplayNode.appleStyleTabContainerNode.reorderedFilterIds + } else { + reorderedFilterIds = self.tabContainerNode.reorderedFilterIds + } + if let reorderedFilterIds = reorderedFilterIds, reorderedFilterIds != defaultFilterIds { reorderedFilterIdsValue = reorderedFilterIds } @@ -3682,12 +3776,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private func reloadFilters(firstUpdate: (() -> Void)? = nil) { let filterItems = chatListFilterItems(context: self.context) var notifiedFirstUpdate = false + + // MARK: Swiftgram + let experimentalUISettingsKey: ValueBoxKey = ApplicationSpecificSharedDataKeys.experimentalUISettings + let displayTabsAtBottomSignal = self.context.sharedContext.accountManager.sharedData(keys: Set([experimentalUISettingsKey])) + |> map { sharedData -> Bool in + let settings: ExperimentalUISettings = sharedData.entries[experimentalUISettingsKey]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + return settings.foldersTabAtBottom + } + |> distinctUntilChanged + self.filterDisposable.set((combineLatest(queue: .mainQueue(), + displayTabsAtBottomSignal, filterItems, self.context.account.postbox.peerView(id: self.context.account.peerId), self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false)) ) - |> deliverOnMainQueue).startStrict(next: { [weak self] countAndFilterItems, peerView, limits in + |> deliverOnMainQueue).startStrict(next: { [weak self] displayTabsAtBottom, countAndFilterItems, peerView, limits in guard let strongSelf = self else { return } @@ -3725,13 +3830,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let firstItem = countAndFilterItems.1.first?.0 ?? .allChats - let firstItemEntryId: ChatListFilterTabEntryId + var firstItemEntryId: ChatListFilterTabEntryId switch firstItem { case .allChats: firstItemEntryId = .all case let .filter(id, _, _, _): firstItemEntryId = .filter(id) } + // MARK: Swiftgram + if !strongSelf.initializedFilters && SGSimpleSettings.shared.rememberLastFolder { + if let lastFolder = SGSimpleSettings.shared.lastAccountFolders["\(strongSelf.context.account.peerId.id._internalGetInt64Value())"]{ + firstItemEntryId = lastFolder == -1 ? .all : .filter(lastFolder) + } + } var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.mainContainerNode.currentItemFilter var resetCurrentEntry = false @@ -3756,7 +3867,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } let filtersLimit = isPremium == false ? limits.maxFoldersCount : nil - strongSelf.tabContainerData = (resolvedItems, false, filtersLimit) + strongSelf.tabContainerData = (resolvedItems, displayTabsAtBottom, filtersLimit) var availableFilters: [ChatListContainerNodeFilter] = [] var hasAllChats = false for item in items { @@ -3805,6 +3916,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } else { strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.mainContainerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + // MARK: Swiftgram + strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0 * (SGSimpleSettings.shared.hideTabBar ? 3.0 : 1.0)), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.mainContainerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + strongSelf.chatListDisplayNode.appleStyleTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.currentState.editing, /*canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit,*/ transitionFraction: strongSelf.chatListDisplayNode.mainContainerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) } } @@ -4507,7 +4621,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion?() - (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) + (self.parent as? TabBarController)?.updateIsTabBarHidden(SGSimpleSettings.shared.hideTabBar ? true : false, transition: .animated(duration: 0.4, curve: .spring)) self.isSearchActive = false if let navigationController = self.navigationController as? NavigationController { @@ -6155,11 +6269,15 @@ private final class ChatListLocationContext { var leftButton: AnyComponentWithIdentity? var rightButton: AnyComponentWithIdentity? + var settingsButton: AnyComponentWithIdentity? var proxyButton: AnyComponentWithIdentity? var storyButton: AnyComponentWithIdentity? var rightButtons: [AnyComponentWithIdentity] { var result: [AnyComponentWithIdentity] = [] + if let settingsButton = self.settingsButton { + result.append(settingsButton) + } if let rightButton = self.rightButton { result.append(rightButton) } @@ -6206,6 +6324,14 @@ private final class ChatListLocationContext { return lhs == rhs }) + // MARK: Swiftgram + let hideStoriesSignal = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings]) + |> map { view -> Bool in + let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default + return settings.hideStories + } + |> distinctUntilChanged + let passcode = context.sharedContext.accountManager.accessChallengeData() |> map { view -> (Bool, Bool) in let data = view.data @@ -6274,6 +6400,7 @@ private final class ChatListLocationContext { case .chatList: if !hideNetworkActivityStatus { self.titleDisposable = combineLatest(queue: .mainQueue(), + hideStoriesSignal, networkState, hasProxy, passcode, @@ -6282,12 +6409,13 @@ private final class ChatListLocationContext { peerStatus, parentController.updatedPresentationData.1, storyPostingAvailable - ).startStrict(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus, presentationData, storyPostingAvailable in + ).startStrict(next: { [weak self] hideStories, networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus, presentationData, storyPostingAvailable in guard let self else { return } self.updateChatList( + hideStories: hideStories, networkState: networkState, proxy: proxy, passcode: passcode, @@ -6486,7 +6614,9 @@ private final class ChatListLocationContext { } var transition: ContainedViewLayoutTransition = .immediate let previousToolbar = previousToolbarValue.swap(toolbar) - if (previousToolbar == nil) != (toolbar == nil) { + if SGSimpleSettings.shared.hideTabBar { + transition = .animated(duration: 0.2, curve: .easeInOut) + } else if (previousToolbar == nil) != (toolbar == nil) { transition = .animated(duration: 0.4, curve: .spring) } if strongSelf.toolbar != toolbar { @@ -6504,6 +6634,7 @@ private final class ChatListLocationContext { } private func updateChatList( + hideStories: Bool, networkState: AccountNetworkState, proxy: (Bool, Bool), passcode: (Bool, Bool), @@ -6620,7 +6751,7 @@ private final class ChatListLocationContext { } } - if storyPostingAvailable { + if storyPostingAvailable && !hideStories { self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent( content: .icon(imageName: "Chat List/AddStoryIcon"), pressed: { [weak self] _ in @@ -6633,6 +6764,29 @@ private final class ChatListLocationContext { } else { self.storyButton = nil } + + // MARK: Swiftgram + if SGSimpleSettings.shared.hideTabBar { + self.settingsButton = AnyComponentWithIdentity(id: "settings", component: AnyComponent(NavigationButtonComponent( + content: .more, + pressed: { [weak self] _ in + self?.parentController?.settingsPressed() + }, + contextAction: { [weak self] sourceView, gesture in + guard let self else { + return + } + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + if let accountSettingsController = rootController.accountSettingsController { + accountSettingsController.tabBarItemContextAction(sourceView: sourceView, gesture: gesture) + } + } + } + ))) + } else { + self.settingsButton = nil + } + } else { let parentController = self.parentController self.rightButton = AnyComponentWithIdentity(id: "more", component: AnyComponent(NavigationButtonComponent( @@ -6848,3 +7002,15 @@ private final class ChatListLocationContext { } } } + +// MARK: Swiftgram +extension ChatListControllerImpl { + + @objc fileprivate func settingsPressed() { + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + if let accountSettingsController = rootController.accountSettingsController { + (self.navigationController as? NavigationController)?.pushViewController(accountSettingsController) + } + } + } +} diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 2b628cdf7df..91f5bb253ad 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -480,8 +481,8 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode) - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in - guard let self, self.availableFilters.count > 1 || (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false)) else { + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in // MARK: Swiftgram + guard let self, self.availableFilters.count > 1 || (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) && !SGSimpleSettings.shared.disableSwipeToRecordStory) else { return [] } guard case .chatList(.root) = self.location else { @@ -503,7 +504,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele } else { return [.rightEdge] } - }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) + }, edgeWidth: SGSimpleSettings.shared.disableChatSwipeOptions ? .widthMultiplier(factor: 1.0 / 6.0, min: 0.0, max: 0.0) : .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) panRecognizer.delegate = self.wrappedGestureRecognizerDelegate panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true @@ -532,8 +533,13 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { - let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count) - let maxFilterIndex = min(Int(filtersLimit), self.availableFilters.count) - 1 + // MARK: Swiftgram + var _availableFilters = self.availableFilters + if SGSimpleSettings.shared.allChatsHidden { + _availableFilters.removeAll { $0 == .all } + } + let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(_availableFilters.count) + let maxFilterIndex = min(Int(filtersLimit), _availableFilters.count) - 1 switch recognizer.state { case .began: @@ -566,7 +572,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele } } case .changed: - if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { + if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = _availableFilters.firstIndex(where: { $0.id == self.selectedId }) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / layout.size.width @@ -578,8 +584,8 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele let coefficient: CGFloat = 0.4 return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } - - if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) { + // MARK: Swiftgram + if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) && !SGSimpleSettings.shared.disableSwipeToRecordStory { let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false if selectedIndex <= 0 && translation.x > 0.0 { transitionFraction = 0.0 @@ -626,7 +632,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false) } case .cancelled, .ended: - if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) { + if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = _availableFilters.firstIndex(where: { $0.id == self.selectedId }) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? @@ -663,7 +669,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele } else { updatedIndex = max(updatedIndex - 1, 0) } - let switchToId = self.availableFilters[updatedIndex].id + let switchToId = _availableFilters[updatedIndex].id if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] { let _ = itemNode self.selectedId = switchToId @@ -1039,6 +1045,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer + // MARK: Swiftgram + let inlineTabContainerNode: ChatListFilterTabContainerNode + let appleStyleTabContainerNode: AppleStyleFoldersNode + let mainContainerNode: ChatListContainerNode var effectiveContainerNode: ChatListContainerNode { @@ -1119,6 +1129,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { openArchiveSettings?() }) + // MARK: Swiftgram + self.inlineTabContainerNode = ChatListFilterTabContainerNode(inline: true, context: context) + self.appleStyleTabContainerNode = AppleStyleFoldersNode() + self.controller = controller super.init() @@ -1131,6 +1145,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.addSubnode(self.mainContainerNode) + // MARK: Swiftgram + self.addSubnode(self.inlineTabContainerNode) + self.addSubnode(self.appleStyleTabContainerNode) + self.mainContainerNode.contentOffsetChanged = { [weak self] offset, listView in self?.contentOffsetChanged(offset: offset, listView: listView, isPrimary: true) } @@ -1587,6 +1605,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { cleanMainNavigationBarHeight = visualNavigationHeight mainInsets.top = visualNavigationHeight } + // MARK: Swiftgram + if !self.inlineTabContainerNode.isHidden { + mainInsets.bottom += 46.0 + } else if !self.appleStyleTabContainerNode.isHidden { mainInsets.bottom += 50.0 } self.mainContainerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, storiesInset: storiesInset, transition: transition) if let inlineStackContainerNode = self.inlineStackContainerNode { @@ -1620,6 +1642,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } } + // MARK: Swiftgram + transition.updateFrame(node: self.inlineTabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - layout.intrinsicInsets.bottom - 46.0), size: CGSize(width: layout.size.width, height: 46.0))) + transition.updateFrame(node: self.appleStyleTabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - layout.intrinsicInsets.bottom - 8.0 - 40.0), size: CGSize(width: layout.size.width, height: 40.0))) self.tapRecognizer?.isEnabled = self.isReorderingFilters if let searchDisplayController = self.searchDisplayController { diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index dadb6d7ec4a..0bda3c3a737 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import Display import TelegramCore import TelegramPresentationData +import AccountContext +import SGSimpleSettings private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { private let pressed: () -> Void @@ -304,6 +306,11 @@ private final class ItemNode: ASDisplayNode { } func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) { + // MARK: Swiftgram + var height = height + if SGSimpleSettings.shared.hideTabBar { + height = 46.0 + } let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) let _ = self.titleActiveNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude)) let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left, y: floor((height - titleSize.height) / 2.0)), size: titleSize) @@ -350,6 +357,11 @@ private final class ItemNode: ASDisplayNode { } func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) { + // MARK: Swiftgram + var size = size + if SGSimpleSettings.shared.hideTabBar { + size.height = 46.0 + } transition.updateAlpha(node: self.titleContainer, alpha: useShortTitle ? 0.0 : 1.0) transition.updateAlpha(node: self.shortTitleContainer, alpha: useShortTitle ? 1.0 : 0.0) @@ -546,13 +558,26 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } } - public override init() { + // MARK: Swiftgram + public let inline: Bool + private var backgroundNode: NavigationBackgroundNode? = nil + + public init(inline: Bool = false, context: AccountContext? = nil) { self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() self.selectedLineNode.displaysAsynchronously = false self.selectedLineNode.displayWithoutProcessing = true + // MARK: Swiftgram + self.inline = inline + if self.inline { + if let context = context { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.backgroundNode = NavigationBackgroundNode(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor) + } + } + super.init() self.scrollNode.view.showsHorizontalScrollIndicator = false @@ -563,7 +588,9 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } - + if let backgroundNode = self.backgroundNode { + self.addSubnode(backgroundNode) + } self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.selectedLineNode) @@ -711,13 +738,25 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { + if let backgroundNode = self.backgroundNode { + backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + } self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) - context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) - context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) + if self.inline { + // Draw ellipses at the bottom corners + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0, y: size.height - 4.0), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: size.height - 4.0), size: CGSize(width: 4.0, height: 4.0))) + // Draw rectangles to connect the ellipses + context.fill(CGRect(x: 2.0, y: size.height - 4.0, width: size.width - 4.0, height: 4.0)) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height - 2.0)) + } else { + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) + context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) + context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) + } })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch) } @@ -746,6 +785,11 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.reorderingGesture?.isEnabled = isReordering + // MARK: Swiftgram + if let backgroundNode = self.backgroundNode { + transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + backgroundNode.update(size: backgroundNode.bounds.size, transition: transition) + } transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) enum BadgeAnimation { @@ -831,7 +875,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { selectionFraction = 0.0 } - itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition) + itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition) } var removeKeys: [ChatListFilterTabEntryId] = [] for (id, _) in self.itemNodes { @@ -878,7 +922,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } } - let minSpacing: CGFloat = 26.0 + let minSpacing: CGFloat = 26.0 / (SGSimpleSettings.shared.compactFolderNames ? 2.5 : 1.0) let resolvedSideInset: CGFloat = 16.0 + sideInset var leftOffset: CGFloat = resolvedSideInset @@ -901,7 +945,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { itemNodeTransition = .immediate } - let useShortTitle = itemId == .all && useShortTitles + let useShortTitle = itemId == .all && sgUseShortAllChatsTitle(useShortTitles) let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) @@ -955,7 +999,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { if let selectedFrame = selectedFrame { let wasAdded = self.selectedLineNode.isHidden self.selectedLineNode.isHidden = false - let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0)) + let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: self.inline ? 0.0 : size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0)) if wasAdded { self.selectedLineNode.frame = lineFrame } else { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3620d86c213..2a96b6cb24f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3519,7 +3519,14 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var result: [ChatListRecentEntry] = [] var existingIds = Set() + // MARK: Swiftgram + // Hidding SwiftgramBot from recents so it won't annoy people. Ideally we should call removeRecentlyUsedApp, so it won't annoy users in other apps + let skipId = 5846791198 + for id in localApps.peerIds { + if id.id._internalGetInt64Value() == skipId { + continue + } if existingIds.contains(id) { continue } @@ -3559,6 +3566,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } if let remoteApps { for appPeerId in remoteApps { + if appPeerId.id._internalGetInt64Value() == skipId { + continue + } if existingIds.contains(appPeerId) { continue } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 852175365b8..63438a5cf71 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -618,7 +619,7 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem } } } - if canDelete { + if canDelete && !SGSimpleSettings.shared.disableDeleteChatSwipeOption { options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) } if case .savedMessagesChats = location { @@ -706,7 +707,7 @@ private func forumThreadRevealOptions(strings: PresentationStrings, theme: Prese } } } - if canDelete { + if canDelete && !SGSimpleSettings.shared.disableDeleteChatSwipeOption { options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) } if canOpenClose { @@ -1615,7 +1616,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if case let .groupReference(groupReference) = item.content { storyState = groupReference.storyState } - + // MARK: Swiftgram + let sgCompactChatList = SGSimpleSettings.shared.compactChatList var peer: EnginePeer? var displayAsMessage = false var enablePreview = true @@ -1680,8 +1682,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { isForumAvatar = true } } - - var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) + // MARK: Swiftgram + var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) / (sgCompactChatList ? 1.5 : 1.0) if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { avatarDiameter = 40.0 @@ -1897,6 +1899,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult let currentCustomTextEntities = self.cachedCustomTextEntities + + // MARK: Swiftgram + let sgCompactChatList = SGSimpleSettings.shared.compactChatList + return { item, params, first, last, firstWithHeader, nextIsPinned in let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) @@ -2124,9 +2130,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } let enableChatListPhotos = true - + // MARK: Swiftgram // if changed, adjust setupItem accordingly - var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) + var avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) / (sgCompactChatList ? 1.5 : 1.0) let avatarLeftInset: CGFloat if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.commandPrefix != nil { @@ -2194,7 +2200,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { contentData = .group(peers: groupPeers) hideAuthor = true } - + // MARK: Swiftgram + if sgCompactChatList { hideAuthor = true }; var attributedText: NSAttributedString var hasDraft = false @@ -2222,7 +2229,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } } - + // MARK: Swiftgram + if sgCompactChatList { useInlineAuthorPrefix = true }; if useInlineAuthorPrefix { if case let .user(author) = messages.last?.author { if author.id == item.context.account.peerId { @@ -3150,12 +3158,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { textMaxWidth -= 18.0 } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && !sgCompactChatList) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) let maxTitleLines: Int switch item.index { case .forum: + // MARK: Swiftgram + if sgCompactChatList { maxTitleLines = 1 } else { maxTitleLines = 2 + } case .chatList: maxTitleLines = 1 } @@ -3277,6 +3288,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor), ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor) ] + if SGSimpleSettings.shared.disableDeleteChatSwipeOption { peerRevealOptions.removeLast() } } else { peerRevealOptions = [] } @@ -3326,7 +3338,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { itemHeight += titleSpacing itemHeight += authorSpacing } - + // MARK: Swiftgram + itemHeight = itemHeight / (sgCompactChatList ? 1.5 : 1.0) let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0)) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) @@ -3481,7 +3494,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarScaleOffset: CGFloat = 0.0 var avatarScale: CGFloat = 1.0 if let inlineNavigationLocation = item.interaction.inlineNavigationLocation { - let targetAvatarScale: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 54.0 / 17.0) / avatarFrame.width + // MARK: Swiftgram + let targetAvatarScale: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 54.0 / 17.0) / (sgCompactChatList ? 1.5 : 1.0) / avatarFrame.width avatarScale = targetAvatarScale * inlineNavigationLocation.progress + 1.0 * (1.0 - inlineNavigationLocation.progress) let targetAvatarScaleOffset: CGFloat = -(avatarFrame.width - avatarFrame.width * avatarScale) * 0.5 @@ -3817,11 +3831,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent) if let _ = currentBadgeBackgroundImage { - let badgeFrame = CGRect(x: contentRect.maxX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height) + // MARK: Swiftgram + let sizeFactor = item.presentationData.fontSize.itemListBaseFontSize / 17.0 + let badgeFrame = CGRect(x: contentRect.maxX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + (sgCompactChatList ? 13.0 / sizeFactor : 0.0), width: badgeLayout.width, height: badgeLayout.height) transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame) } - + // MARK: Swiftgram + let sizeFactor = item.presentationData.fontSize.itemListBaseFontSize / 17.0 if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil { let mentionBadgeOffset: CGFloat if badgeLayout.width.isZero { @@ -3830,7 +3847,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { mentionBadgeOffset = contentRect.maxX - badgeLayout.width - 6.0 - mentionBadgeLayout.width } - let badgeFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) + let badgeFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + (sgCompactChatList ? 13.0 / sizeFactor : 0.0), width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame) } @@ -3840,7 +3857,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.pinnedIconNode.isHidden = false let pinnedIconSize = currentPinnedIconImage.size - let pinnedIconFrame = CGRect(x: contentRect.maxX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height) + // MARK: Swiftgram + let pinnedIconFrame = CGRect(x: contentRect.maxX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + (sgCompactChatList ? 13.0 / sizeFactor : 0.0), width: pinnedIconSize.width, height: pinnedIconSize.height) strongSelf.pinnedIconNode.frame = pinnedIconFrame } else { @@ -4453,7 +4471,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: CGSize(width: layout.contentSize.width, height: itemHeight), leftInset: params.leftInset, rightInset: params.rightInset) - if item.editing { + if item.editing || SGSimpleSettings.shared.disableChatSwipeOptions { strongSelf.setRevealOptions((left: [], right: []), enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) } else { strongSelf.setRevealOptions((left: peerLeftRevealOptions, right: peerRevealOptions), enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9d7d9702859..139570a380b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1,3 +1,5 @@ +import SGAPIToken +import SGAPIWebSettings import Foundation import UIKit import Display @@ -73,6 +75,7 @@ public final class ChatListNodeInteraction { } let activateSearch: () -> Void + let openSGAnnouncement: (String, String, Bool, Bool) -> Void let peerSelected: (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void let disabledPeerSelected: (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void let togglePeerSelected: (EnginePeer, Int64?) -> Void @@ -129,6 +132,8 @@ public final class ChatListNodeInteraction { animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, activateSearch: @escaping () -> Void, + // MARK: Swiftgram + openSGAnnouncement: @escaping (String, String, Bool, Bool) -> Void = { _, _, _, _ in }, peerSelected: @escaping (EnginePeer, EnginePeer?, Int64?, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, togglePeerSelected: @escaping (EnginePeer, Int64?) -> Void, @@ -170,6 +175,7 @@ public final class ChatListNodeInteraction { editPeer: @escaping (ChatListItem) -> Void ) { self.activateSearch = activateSearch + self.openSGAnnouncement = openSGAnnouncement self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected self.togglePeerSelected = togglePeerSelected @@ -734,6 +740,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL switch action { case .activate: switch notice { + case let .sgUrl(id, _, _, url, needAuth, permanent): + nodeInteraction?.openSGAnnouncement(id, url, needAuth, permanent) case .clearStorage: nodeInteraction?.openStorageManagement() case .setupPassword: @@ -747,6 +755,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case .setupBirthday: nodeInteraction?.openBirthdaySetup() case let .birthdayPremiumGift(_, birthdays): + // TODO(swiftgram): Open user's profile instead of gift nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break @@ -1074,6 +1083,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL switch action { case .activate: switch notice { + case let .sgUrl(id, _, _, url, needAuth, permanent): + nodeInteraction?.openSGAnnouncement(id, url, needAuth, permanent) case .clearStorage: nodeInteraction?.openStorageManagement() case .setupPassword: @@ -1338,6 +1349,9 @@ public final class ChatListNode: ListView { public var startedScrollingAtUpperBound: Bool = false + // MARK: Swiftgram + public var getNavigationController: (()-> NavigationController?)? + private let autoSetReady: Bool public let isMainTab = ValuePromise(false, ignoreRepeated: true) @@ -1345,8 +1359,9 @@ public final class ChatListNode: ListView { public var synchronousDrawingWhenNotAnimated: Bool = false - public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool, isMainTab: Bool?) { + public init(getNavigationController: (() -> NavigationController?)? = nil, context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool, isMainTab: Bool?) { self.context = context + self.getNavigationController = getNavigationController self.location = location self.chatListFilter = chatListFilter self.chatListFilterValue.set(.single(chatListFilter)) @@ -1387,6 +1402,31 @@ public final class ChatListNode: ListView { if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() } + }, openSGAnnouncement: { [weak self] announcementId, url, needAuth, permanent in + if let strongSelf = self { + if needAuth { + let _ = (getSGSettingsURL(context: strongSelf.context, url: url) + |> deliverOnMainQueue).start(next: { [weak self] url in + guard let strongSelf = self else { + return + } + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: false, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: strongSelf.getNavigationController?(), dismissInput: {}) + }) + } else { + Queue.mainQueue().async { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: false, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: strongSelf.getNavigationController?(), dismissInput: {}) + } + + } + if !permanent { + Queue.mainQueue().after(0.6) { [weak self] in + if let strongSelf = self { + dismissSGProvidedSuggestion(suggestionId: announcementId) + postSGWebSettingsInteractivelly(context: strongSelf.context, data: ["skip_announcement_id": announcementId]) + } + } + } + } }, peerSelected: { [weak self] peer, _, threadId, promoInfo in if let strongSelf = self, let peerSelected = strongSelf.peerSelected { peerSelected(peer, threadId, true, true, promoInfo) @@ -1938,6 +1978,7 @@ public final class ChatListNode: ListView { let twoStepData: Signal = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) let suggestedChatListNoticeSignal: Signal = combineLatest( + getSGProvidedSuggestions(account: context.account), context.engine.notices.getServerProvidedSuggestions(), context.engine.notices.getServerDismissedSuggestions(), twoStepData, @@ -1946,7 +1987,14 @@ public final class ChatListNode: ListView { context.account.stateManager.contactBirthdays, starsSubscriptionsContextPromise.get() ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays, starsSubscriptionsContext -> Signal in + |> mapToSignal { sgSuggestionsData, suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays, starsSubscriptionsContext -> Signal in + // MARK: Swiftgam + if let sgSuggestionsData = sgSuggestionsData, let dictionary = try? JSONSerialization.jsonObject(with: sgSuggestionsData, options: []), let sgSuggestions = dictionary as? [[String: Any]], let sgSuggestion = sgSuggestions.first, let sgSuggestionId = sgSuggestion["id"] as? String { + if let sgSuggestionType = sgSuggestion["type"] as? String, sgSuggestionType == "SG_URL", let sgSuggestionTitle = sgSuggestion["title"] as? String, let sgSuggestionUrl = sgSuggestion["url"] as? String { + return .single(.sgUrl(id: sgSuggestionId, title: sgSuggestionTitle, text: sgSuggestion["text"] as? String, url: sgSuggestionUrl, needAuth: sgSuggestion["need_auth"] as? Bool ?? false, permanent: sgSuggestion["permanent"] as? Bool ?? false)) + + } + } if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } @@ -2002,8 +2050,12 @@ public final class ChatListNode: ListView { } else if suggestions.contains(.setupBirthday) && birthday == nil { return .single(.setupBirthday) } else if suggestions.contains(.xmasPremiumGift) { + // MARK: Swiftgram + if ({ return true }()) { return .single(nil) } return .single(.xmasPremiumGift) } else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { + // MARK: Swiftgram + if ({ return true }()) { return .single(nil) } return inAppPurchaseManager.availableProducts |> map { products -> ChatListNotice? in if products.count > 1 { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 603a3430d40..fe1685ffa45 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -81,6 +81,7 @@ public enum ChatListNodeEntryPromoInfo: Equatable { public enum ChatListNotice: Equatable { case clearStorage(sizeFraction: Double) + case sgUrl(id: String, title: String, text: String?, url: String, needAuth: Bool, permanent: Bool) case setupPassword case premiumUpgrade(discount: Int32) case premiumAnnualDiscount(discount: Int32) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index cb37b982214..381327f3ff8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -181,6 +181,12 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { var alignment: NSTextAlignment = .left switch item.notice { + // MARK: Swiftgram + case let .sgUrl(_, title, text, _, _, _): + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + titleString = titleStringValue + + textString = NSAttributedString(string: text ?? "", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case let .clearStorage(sizeFraction): let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: ".")) let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString) diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 6f4f9066ee7..f2ba556d222 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -71,9 +71,9 @@ public final class ChatPanelInterfaceInteraction { public let reportMessages: ([Message], ContextControllerProtocol?) -> Void public let blockMessageAuthor: (Message, ContextControllerProtocol?) -> Void public let deleteMessages: ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void - public let forwardSelectedMessages: () -> Void + public let forwardSelectedMessages: (String?) -> Void public let forwardCurrentForwardMessages: () -> Void - public let forwardMessages: ([Message]) -> Void + public let forwardMessages: ([Message], String?) -> Void public let updateForwardOptionsState: ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void public let presentForwardOptions: (ASDisplayNode) -> Void public let presentReplyOptions: (ASDisplayNode) -> Void @@ -186,9 +186,9 @@ public final class ChatPanelInterfaceInteraction { reportMessages: @escaping ([Message], ContextControllerProtocol?) -> Void, blockMessageAuthor: @escaping (Message, ContextControllerProtocol?) -> Void, deleteMessages: @escaping ([Message], ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void, - forwardSelectedMessages: @escaping () -> Void, + forwardSelectedMessages: @escaping (String?) -> Void, forwardCurrentForwardMessages: @escaping () -> Void, - forwardMessages: @escaping ([Message]) -> Void, + forwardMessages: @escaping ([Message], String?) -> Void, updateForwardOptionsState: @escaping ((ChatInterfaceForwardOptionsState) -> ChatInterfaceForwardOptionsState) -> Void, presentForwardOptions: @escaping (ASDisplayNode) -> Void, presentReplyOptions: @escaping (ASDisplayNode) -> Void, @@ -422,9 +422,9 @@ public final class ChatPanelInterfaceInteraction { }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) - }, forwardSelectedMessages: { + }, forwardSelectedMessages: { _ in }, forwardCurrentForwardMessages: { - }, forwardMessages: { _ in + }, forwardMessages: { _, _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in }, presentReplyOptions: { _ in diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 9e87f7d610d..e973a9c91ff 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -66,6 +66,7 @@ public enum SendMessageActionSheetControllerParams { } public func makeChatSendMessageActionSheetController( + sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: nil, translate: nil, changeTranslationLanguage: nil), initialData: ChatSendMessageContextScreen.InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -87,6 +88,7 @@ public func makeChatSendMessageActionSheetController( isPremium: Bool = false ) -> ChatSendMessageActionSheetController { return ChatSendMessageContextScreen( + sgTranslationContext: sgTranslationContext, initialData: initialData, context: context, updatedPresentationData: updatedPresentationData, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift deleted file mode 100644 index 8b137891791..00000000000 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 29125b33ac0..e0d94a98386 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -53,6 +53,7 @@ public protocol ChatSendMessageContextScreenMediaPreview: AnyObject { final class ChatSendMessageContextScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment + let sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) let initialData: ChatSendMessageContextScreen.InitialData let context: AccountContext let updatedPresentationData: (initial: PresentationData, signal: Signal)? @@ -72,8 +73,9 @@ final class ChatSendMessageContextScreenComponent: Component { let reactionItems: [ReactionItem]? let availableMessageEffects: AvailableMessageEffects? let isPremium: Bool - + // MARK: Swiftgram init( + sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: nil, translate: nil, changeTranslationLanguage: nil), initialData: ChatSendMessageContextScreen.InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, @@ -94,6 +96,7 @@ final class ChatSendMessageContextScreenComponent: Component { availableMessageEffects: AvailableMessageEffects?, isPremium: Bool ) { + self.sgTranslationContext = sgTranslationContext self.initialData = initialData self.context = context self.updatedPresentationData = updatedPresentationData @@ -631,6 +634,78 @@ final class ChatSendMessageContextScreenComponent: Component { ))) } + // MARK: Swiftgram + if !isSecret { + if let outgoingMessageTranslateToLang = component.sgTranslationContext.outgoingMessageTranslateToLang { + var languageCode = presentationData.strings.baseLanguageCode + let rawSuffix = "-raw" + if languageCode.hasSuffix(rawSuffix) { + languageCode = String(languageCode.dropLast(rawSuffix.count)) + } + + // Assuming, user want to send message in the same language the chat is + let toLang = outgoingMessageTranslateToLang + let key = "Translation.Language.\(toLang)" + let translateTitle: String + if let string = presentationData.strings.primaryComponent.dict[key] { + translateTitle = presentationData.strings.Conversation_Translation_TranslateTo(string).string + } else { + let languageLocale = Locale(identifier: languageCode) + let toLanguage = languageLocale.localizedString(forLanguageCode: toLang) ?? "" + translateTitle = presentationData.strings.Conversation_Translation_TranslateToOther(toLanguage).string + } + + items.append(.action(ContextMenuActionItem( + id: AnyHashable("sgTranslate"), + text: translateTitle, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + + component.sgTranslationContext.translate?() + self.environment?.controller()?.dismiss() + } + ))) + + items.append(.action(ContextMenuActionItem( + id: AnyHashable("sgChangeTranslateLang"), + text: presentationData.strings.Translate_ChangeLanguage, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + + self.environment?.controller()?.dismiss() + component.sgTranslationContext.changeTranslationLanguage?() + } + ))) + + } else { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("sgChangeTranslateLang"), + text: presentationData.strings.Conversation_Translation_TranslateToOther("...").string, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + + self.environment?.controller()?.dismiss() + component.sgTranslationContext.changeTranslationLanguage?() + } + ))) + } + } + if case .separator = items.last { items.removeLast() } @@ -1413,6 +1488,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha } public init( + sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: nil, translate: nil, changeTranslationLanguage: nil), initialData: InitialData, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, @@ -1438,6 +1514,7 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha super.init( context: context, component: ChatSendMessageContextScreenComponent( + sgTranslationContext: sgTranslationContext, initialData: initialData, context: context, updatedPresentationData: updatedPresentationData, diff --git a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift index 186e7ded8ce..2acf3fbd5fc 100644 --- a/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift +++ b/submodules/CountrySelectionUI/Sources/AuthorizationSequenceCountrySelectionController.swift @@ -73,6 +73,8 @@ private func loadCountryCodes() -> [Country] { private var countryCodes: [Country] = loadCountryCodes() private var countryCodesByPrefix: [String: (Country, Country.CountryCode)] = [:] +// MARK: Swiftgram +private var sgCountryCodesByPrefix: [String: (Country, Country.CountryCode)] = ["999": (Country(id: "XX", name: "Demo", localizedName: nil, countryCodes: [Country.CountryCode(code: "999", prefixes: [], patterns: ["XX X XXXX"])], hidden: false), Country.CountryCode(code: "999", prefixes: [], patterns: ["XX X XXXX"]))] public func loadServerCountryCodes(accountManager: AccountManager, engine: TelegramEngineUnauthorized, completion: @escaping () -> Void) { let _ = (engine.localization.getCountriesList(accountManager: accountManager, langCode: nil) @@ -230,7 +232,7 @@ public final class AuthorizationSequenceCountrySelectionController: ViewControll for i in 0.. country.1.code.count { break diff --git a/submodules/CountrySelectionUI/Sources/CountryList.swift b/submodules/CountrySelectionUI/Sources/CountryList.swift index 87efdf89bbd..1c571d24c42 100644 --- a/submodules/CountrySelectionUI/Sources/CountryList.swift +++ b/submodules/CountrySelectionUI/Sources/CountryList.swift @@ -9,6 +9,8 @@ public func emojiFlagForISOCountryCode(_ countryCode: String) -> String { if countryCode == "FT" { return "🏴‍☠️" + } else if countryCode == "XX" { + return "🏳️" } else if countryCode == "XG" { return "🛰️" } else if countryCode == "XV" { diff --git a/submodules/DebugSettingsUI/BUILD b/submodules/DebugSettingsUI/BUILD index 60710882b03..ccea64e55a9 100644 --- a/submodules/DebugSettingsUI/BUILD +++ b/submodules/DebugSettingsUI/BUILD @@ -1,5 +1,11 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGDebugUI:SGDebugUI", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "DebugSettingsUI", module_name = "DebugSettingsUI", @@ -9,7 +15,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/Postbox:Postbox", diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index db37d2bed4b..b21a2063c1a 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -1,3 +1,8 @@ +// MARK: Swiftgram +import SGLogging +import SGSimpleSettings +import SGDebugUI + import Foundation import UIKit import Display @@ -44,6 +49,7 @@ private final class DebugControllerArguments { } private enum DebugControllerSection: Int32 { + case swiftgram case sticker case logs case logging @@ -56,6 +62,8 @@ private enum DebugControllerSection: Int32 { } private enum DebugControllerEntry: ItemListNodeEntry { + case SGDebug(PresentationTheme) + case sendSGLogs(PresentationTheme) case testStickerImport(PresentationTheme) case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) @@ -118,6 +126,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { + case .sendSGLogs, .SGDebug: + return DebugControllerSection.swiftgram.rawValue case .testStickerImport: return DebugControllerSection.sticker.rawValue case .sendLogs, .sendOneLog, .sendShareLogs, .sendGroupCallLogs, .sendStorageStats, .sendNotificationLogs, .sendCriticalLogs, .sendAllLogs: @@ -145,6 +155,11 @@ private enum DebugControllerEntry: ItemListNodeEntry { var stableId: Int { switch self { + // MARK: Swiftgram + case .SGDebug: + return -110 + case .sendSGLogs: + return -100 case .testStickerImport: return 0 case .sendLogs: @@ -273,6 +288,13 @@ private enum DebugControllerEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! DebugControllerArguments switch self { + case .SGDebug: + return ItemListDisclosureItem(presentationData: presentationData, title: "Swiftgram Debug", label: "", sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + arguments.pushController(sgDebugController(context: context)) + }) case .testStickerImport: return ItemListActionItem(presentationData: presentationData, title: "Simulate Stickers Import", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { @@ -372,9 +394,20 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(actionSheet, nil) }) }) - case .sendOneLog: - return ItemListDisclosureItem(presentationData: presentationData, title: "Send Latest Logs (Up to 4 MB)", label: "", sectionId: self.section, style: .blocks, action: { - let _ = (Logger.shared.collectLogs() + // MARK: Swiftgram + case .sendOneLog, .sendSGLogs: + var title = "Send Latest Logs (Up to 4 MB)" + var logCollectionSignal: Signal<[(String, String)], NoError> = Logger.shared.collectLogs() + var fileName = "Log-iOS-Short.txt" + var appName = "Telegram" + if case .sendSGLogs(_) = self { + title = "Send Swiftgram Logs" + logCollectionSignal = SGLogger.shared.collectLogs() + fileName = "Log-iOS-Swiftgram.txt" + appName = "Swiftgram" + } + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", sectionId: self.section, style: .blocks, action: { + let _ = (logCollectionSignal |> deliverOnMainQueue).start(next: { logs in let presentationData = arguments.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) @@ -421,7 +454,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let fileResource = LocalFileMediaResource(fileId: id, size: Int64(logData.count), isSecretRelated: false) context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")], alternativeRepresentations: []) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: fileName)], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start() @@ -436,7 +469,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let composeController = MFMailComposeViewController() composeController.mailComposeDelegate = arguments.mailComposeDelegate - composeController.setSubject("Telegram Logs") + composeController.setSubject("\(appName) Logs") for (name, path) in logs { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { composeController.addAttachmentData(data, mimeType: "application/text", fileName: name) @@ -1442,9 +1475,13 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present let isMainApp = sharedContext.applicationBindings.isMainApp + // MARK: Swiftgram + entries.append(.SGDebug(presentationData.theme)) + entries.append(.sendSGLogs(presentationData.theme)) + // entries.append(.testStickerImport(presentationData.theme)) entries.append(.sendLogs(presentationData.theme)) - //entries.append(.sendOneLog(presentationData.theme)) + entries.append(.sendOneLog(presentationData.theme)) entries.append(.sendShareLogs) entries.append(.sendGroupCallLogs) entries.append(.sendNotificationLogs(presentationData.theme)) @@ -1464,7 +1501,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetWebViewCache(presentationData.theme)) entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack)) - #if DEBUG + #if true entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory)) #endif entries.append(.dustEffect(experimentalSettings.dustEffect)) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index b01c2a487dc..e4b0a329bc3 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -369,6 +369,37 @@ public enum DeviceMetrics: CaseIterable, Equatable { if case .iPhoneX = self { return false } - return self.hasTopNotch + // MARK: Swiftgram + return self.hasTopNotch || self.hasDynamicIsland } } + +// MARK: Swifgram +public extension DeviceMetrics { + + var deviceModelCode: String { + var systemInfo = utsname() + uname(&systemInfo) + let modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { + ptr in String.init(validatingUTF8: ptr) + } + } + return modelCode ?? "unknown" + } + + var modelHasDynamicIsland: Bool { + switch self.deviceModelCode { + case "iPhone15,2", // iPhone 14 Pro + "iPhone15,3", // iPhone 14 Pro Max + "iPhone15,4", // iPhone 15 + "iPhone15,5", // iPhone 15 Plus + "iPhone16,1", // iPhone 15 Pro + "iPhone16,2": // iPhone 15 Pro Max + return true + default: + return false + } + } + +} diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index f6ecae5ac1f..8de5c494654 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -299,12 +299,18 @@ public func generateSmallHorizontalStretchableFilledCircleImage(diameter: CGFloa })?.stretchableImage(withLeftCapWidth: Int(diameter / 2), topCapHeight: Int(diameter / 2)) } -public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil) -> UIImage? { + +// MARK: Swiftgram +public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil, customSize: CGSize? = nil) -> UIImage? { guard let image = image else { return nil } - let imageSize = image.size + // MARK: Swiftgram + var imageSize = image.size + if let strongCustomSize = customSize { + imageSize = strongCustomSize + } UIGraphicsBeginImageContextWithOptions(imageSize, backgroundColor != nil, image.scale) if let context = UIGraphicsGetCurrentContext() { diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index b716d17fb40..ed893d236ea 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -1179,7 +1179,25 @@ public class Window1 { if let image = self.badgeView.image { self.updateBadgeVisibility() - self.badgeView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.windowLayout.size.width - image.size.width) / 2.0), y: 5.0), size: image.size) + // MARK: Swiftgram + var badgeOffset: CGFloat + if case self.deviceMetrics = DeviceMetrics.iPhone14ProZoomed { + badgeOffset = self.deviceMetrics.statusBarHeight - DeviceMetrics.iPhone14ProZoomed.statusBarHeight + if self.deviceMetrics.modelHasDynamicIsland { + badgeOffset += 3.0 + } + } else if case self.deviceMetrics = DeviceMetrics.iPhone14ProMaxZoomed { + badgeOffset = self.deviceMetrics.statusBarHeight - DeviceMetrics.iPhone14ProMaxZoomed.statusBarHeight + if self.deviceMetrics.modelHasDynamicIsland { + badgeOffset += 3.0 + } + } else { + badgeOffset = self.deviceMetrics.statusBarHeight - DeviceMetrics.iPhone13ProMax.statusBarHeight + } + if badgeOffset != 0 { + badgeOffset += 3.0 // Centering badge in status bar for Dynamic island devices + } + self.badgeView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.windowLayout.size.width - image.size.width) / 2.0), y: 5.0 + badgeOffset), size: image.size) } } } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 1f1e17ba774..50b785512c6 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -510,6 +510,10 @@ public struct GalleryConfiguration { } static func with(appConfiguration: AppConfiguration) -> GalleryConfiguration { + // MARK: Swiftgram + if appConfiguration.sgWebSettings.global.ytPip { + return GalleryConfiguration(youtubePictureInPictureEnabled: true) + } if let data = appConfiguration.data, let value = data["youtube_pip"] as? String { return GalleryConfiguration(youtubePictureInPictureEnabled: value != "disabled") } else { diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index 19eadbaba7d..e79d1c6913d 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -113,7 +113,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD private var status: MediaResourceStatus? init(context: AccountContext, presentationData: PresentationData) { - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + //if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { let preferences = WKPreferences() preferences.javaScriptEnabled = false let configuration = WKWebViewConfiguration() @@ -122,13 +122,13 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD webView.allowsLinkPreview = false webView.allowsBackForwardNavigationGestures = false self.webView = webView - } else { + /*} else { let _ = registeredURLProtocol let webView = UIWebView() webView.scalesPageToFit = true self.webView = webView - } + }*/ self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData) self.statusNodeContainer = HighlightableButtonNode() diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index bb911f25ea8..16cc32ee023 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -708,6 +708,21 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) }) }))) + // MARK: Swiftgram + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in + f(.default) + + let _ = (SaveToCameraRoll.copyToPasteboard(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + guard let controller = strongSelf.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Conversation_ImageCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + }))) } } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index ac455d91b6f..aa4db98b031 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -2798,16 +2798,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { f(.default) }))) } - - // if #available(iOS 11.0, *) { - // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - // f(.default) - // guard let strongSelf = self else { - // return - // } - // strongSelf.beginAirPlaySetup() - // }))) - // } + // MARK: Swiftgram + if #available(iOS 11.0, *) { + items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let strongSelf = self else { + return + } + strongSelf.beginAirPlaySetup() + }))) + } if let (message, _, _) = strongSelf.contentInfo() { for media in message.media { diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index cd8408de513..ed12aa583c0 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -60,6 +60,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { let label: String let attributedLabel: NSAttributedString? let labelStyle: ItemListDisclosureLabelStyle + let centerLabelAlignment: Bool let additionalDetailLabel: String? let additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor public let sectionId: ItemListSectionId @@ -71,7 +72,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, centerLabelAlignment: Bool = false, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context @@ -85,6 +86,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.labelStyle = labelStyle self.label = label self.attributedLabel = attributedLabel + self.centerLabelAlignment = centerLabelAlignment self.additionalDetailLabel = additionalDetailLabel self.additionalDetailLabelColor = additionalDetailLabelColor self.sectionId = sectionId @@ -649,7 +651,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { if case .semitransparentBadge = item.labelStyle { badgeWidth += 2.0 } - let badgeFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) + let badgeFrame = CGRect(origin: CGPoint(x: item.centerLabelAlignment ? floor((params.width - badgeWidth) / 2.0) : params.width - rightInset - badgeWidth, y: floor((contentSize.height - badgeDiameter) / 2.0)), size: CGSize(width: badgeWidth, height: badgeDiameter)) strongSelf.labelBadgeNode.frame = badgeFrame let labelFrame: CGRect @@ -657,7 +659,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { case .badge: labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0), size: labelLayout.size) case .semitransparentBadge: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0 - UIScreenPixel + floorToScreenPixels((badgeDiameter - labelLayout.size.height) / 2.0)), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: item.centerLabelAlignment ? floor((params.width - badgeWidth + (badgeWidth - labelLayout.size.width)) / 2.0) : params.width - rightInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: badgeFrame.minY + 1.0 - UIScreenPixel + floorToScreenPixels((badgeDiameter - labelLayout.size.height) / 2.0)), size: labelLayout.size) case .detailText, .multilineDetailText: labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) default: diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 81dc1224b23..3c3a8c48c13 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -67,9 +67,10 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let processPaste: ((String) -> String)? let updatedFocus: ((Bool) -> Void)? let cleared: (() -> Void)? + let dismissKeyboardOnEnter: Bool // MARK: Swiftgram public let tag: ItemListItemTag? - public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil, dismissKeyboardOnEnter: Bool = false) { self.context = context self.presentationData = presentationData self.title = title @@ -93,6 +94,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { self.updatedFocus = updatedFocus self.action = action self.cleared = cleared + self.dismissKeyboardOnEnter = dismissKeyboardOnEnter } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -590,6 +592,10 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.item?.action() + // MARK: Swiftgram + if self.item?.dismissKeyboardOnEnter ?? false && self.textNode.textField.canResignFirstResponder { + self.textNode.textField.resignFirstResponder() + } return false } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h index 12a0296d78f..b8e8dca3ce3 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGPhotoEditorValues.h @@ -11,6 +11,6 @@ + (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRectangle:(PGRectangle *)cropRectangle cropOrientation:(UIImageOrientation)cropOrientation cropSize:(CGSize)cropSize enhanceDocument:(bool)enhanceDocument paintingData:(TGPaintingData *)paintingData; -+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif; ++ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif sendAsTelescope:(bool)sendAsTelescope; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 01b5704dddd..04f18d5c6d9 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -29,6 +29,7 @@ @property (nonatomic, readonly) CGFloat cropLockedAspectRatio; @property (nonatomic, readonly) bool cropMirrored; @property (nonatomic, readonly) bool sendAsGif; +@property (nonatomic, readonly) bool sendAsTelescope; @property (nonatomic, readonly) TGPaintingData *paintingData; @property (nonatomic, readonly) NSDictionary *toolValues; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h index b354ce642b6..2452125af41 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h @@ -37,7 +37,7 @@ @property (nonatomic, readonly) UIView *timerButton; -- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages; +- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages canShowTelescope:(bool)canShowTelescope canSendTelescope:(bool)canSendTelescope; - (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel; - (void)setEditorTabPressed:(void (^)(TGPhotoEditorTab tab))editorTabPressed; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h index 26d30bdef6d..804c038fcd2 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryModel.h @@ -46,7 +46,7 @@ @property (nonatomic, readonly) TGMediaSelectionContext *selectionContext; @property (nonatomic, strong) id stickersContext; -- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages; +- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages canShowTelescope:(bool)canShowTelescope canSendTelescope:(bool)canSendTelescope; - (void)presentPhotoEditorForItem:(id)item tab:(TGPhotoEditorTab)tab; - (void)presentPhotoEditorForItem:(id)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots fromRect:(CGRect)fromRect; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h index e35ddc83f28..4dcbb5686ce 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryVideoItemView.h @@ -3,6 +3,8 @@ #import #import +typedef void (^CompletionBlock)(void); + @protocol TGMediaEditableItem; @protocol TGPhotoDrawingEntitiesView; @@ -23,6 +25,7 @@ - (void)setPlayButtonHidden:(bool)hidden animated:(bool)animated; - (void)toggleSendAsGif; +- (void)toggleSendAsTelescope:(bool)canSendAsTelescope dismissParent:(CompletionBlock)dismissParent; - (void)setScrubbingPanelApperanceLocked:(bool)locked; - (void)setScrubbingPanelHidden:(bool)hidden animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h index 1c10e408425..2418f1f7f4f 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h @@ -29,6 +29,8 @@ + (UIImage *)gifActiveIcon; + (UIImage *)muteIcon; + (UIImage *)muteActiveIcon; ++ (UIImage *)telescopeIcon; ++ (UIImage *)telescopeActiveIcon; + (UIImage *)qualityIconForPreset:(TGMediaVideoConversionPreset)preset; + (UIImage *)timerIconForValue:(NSInteger)value; + (UIImage *)eraserIcon; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h index be3bd2aa1d4..64cb80fe1a2 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h @@ -13,6 +13,7 @@ typedef enum TGMediaVideoConversionPresetCompressedVeryHigh, TGMediaVideoConversionPresetAnimation, TGMediaVideoConversionPresetVideoMessage, + TGMediaVideoConversionPresetVideoMessageHD, TGMediaVideoConversionPresetProfileLow, TGMediaVideoConversionPresetProfile, TGMediaVideoConversionPresetProfileHigh, @@ -62,6 +63,7 @@ typedef enum toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif + sendAsTelescope:(bool)sendAsTelescope preset:(TGMediaVideoConversionPreset)preset; @end @@ -70,3 +72,4 @@ typedef TGVideoEditAdjustments TGMediaVideoEditAdjustments; extern const NSTimeInterval TGVideoEditMinimumTrimmableDuration; extern const NSTimeInterval TGVideoEditMaximumGifDuration; +extern const NSTimeInterval TGVideoEditMaximumTelescopeDuration; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h index b6343a64a51..25bd2afc1b1 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h @@ -29,7 +29,7 @@ @property (nonatomic, copy) void(^displaySlowmodeTooltip)(void); @property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); -- (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; +- (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder startWithRearCam:(bool)startWithRearCam; - (void)buttonInteractionUpdate:(CGPoint)value; - (void)setLocked; diff --git a/submodules/LegacyComponents/Sources/PGPhotoEditor.h b/submodules/LegacyComponents/Sources/PGPhotoEditor.h index 5de1bfc9ebc..fe801293beb 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditor.h +++ b/submodules/LegacyComponents/Sources/PGPhotoEditor.h @@ -19,6 +19,7 @@ @property (nonatomic, assign) NSTimeInterval trimStartValue; @property (nonatomic, assign) NSTimeInterval trimEndValue; @property (nonatomic, assign) bool sendAsGif; +@property (nonatomic, assign) bool sendAsTelescope; @property (nonatomic, assign) TGMediaVideoConversionPreset preset; @property (nonatomic, weak) TGPhotoEditorPreviewView *previewOutput; diff --git a/submodules/LegacyComponents/Sources/PGPhotoEditor.m b/submodules/LegacyComponents/Sources/PGPhotoEditor.m index 38a614d6b25..3086f3f4e48 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditor.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditor.m @@ -551,6 +551,7 @@ - (void)importAdjustments:(id)adjustments self.trimStartValue = videoAdjustments.trimStartValue; self.trimEndValue = videoAdjustments.trimEndValue; self.sendAsGif = videoAdjustments.sendAsGif; + self.sendAsTelescope = videoAdjustments.sendAsTelescope; self.preset = videoAdjustments.preset; } @@ -581,13 +582,13 @@ - (void)importAdjustments:(id)adjustments if (!_forVideo) { - return [PGPhotoEditorValues editorValuesWithOriginalSize:self.originalSize cropRect:self.cropRect cropRotation:self.cropRotation cropOrientation:self.cropOrientation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored toolValues:toolValues paintingData:paintingData sendAsGif:self.sendAsGif]; + return [PGPhotoEditorValues editorValuesWithOriginalSize:self.originalSize cropRect:self.cropRect cropRotation:self.cropRotation cropOrientation:self.cropOrientation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored toolValues:toolValues paintingData:paintingData sendAsGif:self.sendAsGif sendAsTelescope:self.sendAsTelescope]; } else { TGVideoEditAdjustments *initialAdjustments = (TGVideoEditAdjustments *)_initialAdjustments; - return [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:self.originalSize cropRect:self.cropRect cropOrientation:self.cropOrientation cropRotation:self.cropRotation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored trimStartValue:initialAdjustments.trimStartValue trimEndValue:initialAdjustments.trimEndValue toolValues:toolValues paintingData:paintingData sendAsGif:self.sendAsGif preset:self.preset]; + return [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:self.originalSize cropRect:self.cropRect cropOrientation:self.cropOrientation cropRotation:self.cropRotation cropLockedAspectRatio:self.cropLockedAspectRatio cropMirrored:self.cropMirrored trimStartValue:initialAdjustments.trimStartValue trimEndValue:initialAdjustments.trimEndValue toolValues:toolValues paintingData:paintingData sendAsGif:self.sendAsGif sendAsTelescope:self.sendAsTelescope preset:self.preset]; } } diff --git a/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m b/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m index 3257dd4b088..2cef6743d25 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m @@ -13,6 +13,7 @@ @implementation PGPhotoEditorValues @synthesize cropMirrored = _cropMirrored; @synthesize paintingData = _paintingData; @synthesize sendAsGif = _sendAsGif; +@synthesize sendAsTelescope = _sendAsTelescope; @synthesize toolValues = _toolValues; + (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRectangle:(PGRectangle *)cropRectangle cropOrientation:(UIImageOrientation)cropOrientation cropSize:(CGSize)cropSize enhanceDocument:(bool)enhanceDocument paintingData:(TGPaintingData *)paintingData @@ -29,7 +30,7 @@ + (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRectangle: } -+ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif ++ (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRect)cropRect cropRotation:(CGFloat)cropRotation cropOrientation:(UIImageOrientation)cropOrientation cropLockedAspectRatio:(CGFloat)cropLockedAspectRatio cropMirrored:(bool)cropMirrored toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif sendAsTelescope:(bool)sendAsTelescope { PGPhotoEditorValues *values = [[PGPhotoEditorValues alloc] init]; values->_originalSize = originalSize; @@ -41,6 +42,7 @@ + (instancetype)editorValuesWithOriginalSize:(CGSize)originalSize cropRect:(CGRe values->_toolValues = toolValues; values->_paintingData = paintingData; values->_sendAsGif = sendAsGif; + values->_sendAsTelescope = sendAsTelescope; return values; } diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index e1b3d911342..c8c815e525d 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1479,7 +1479,7 @@ - (void)presentResultControllerForItem:(id 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName isScheduledMessages:false]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:galleryItems focusItem:focusItem selectionContext:_items.count > 1 ? selectionContext : nil editingContext:editingContext hasCaptions:self.allowCaptions allowCaptionEntities:self.allowCaptionEntities hasTimer:self.hasTimer onlyCrop:_intent == TGCameraControllerPassportIntent || _intent == TGCameraControllerPassportIdIntent || _intent == TGCameraControllerPassportMultipleIntent inhibitDocumentCaptions:self.inhibitDocumentCaptions hasSelectionPanel:true hasCamera:hasCamera recipientName:self.recipientName isScheduledMessages:false canShowTelescope:false canSendTelescope:false]; model.inhibitMute = self.inhibitMute; model.controller = galleryController; model.stickersContext = self.stickersContext; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 94f4f266326..686aa9b0043 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -475,7 +475,7 @@ - (instancetype)initWithContext:(id)context intent:(TGM } id adjustments = [strongSelf->_editingContext adjustmentsForItem:asset]; - if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]] && ((TGMediaVideoEditAdjustments *)adjustments).sendAsGif) + if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]] && (((TGMediaVideoEditAdjustments *)adjustments).sendAsGif || ((TGMediaVideoEditAdjustments *)adjustments).sendAsTelescope)) { onlyGroupableMedia = false; break; @@ -957,7 +957,7 @@ + (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selecti id adjustments = [editingContext adjustmentsForItem:asset]; if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; - if (videoAdjustments.sendAsGif) { + if (videoAdjustments.sendAsGif || videoAdjustments.sendAsTelescope) { grouping = false; } } @@ -1485,7 +1485,7 @@ + (NSArray *)pasteboardResultSignalsForSelectionContext:(TGMediaSelectionContext id adjustments = [editingContext adjustmentsForItem:asset]; if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; - if (videoAdjustments.sendAsGif) { + if (videoAdjustments.sendAsGif || videoAdjustments.sendAsTelescope) { grouping = false; } } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 5ed286a8fd0..46b1c023a3e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -86,6 +86,7 @@ @interface TGMediaPickerGalleryInterfaceView () TGPhotoCaptionInputMixin *_captionMixin; TGModernButton *_muteButton; + TGModernButton *_telescopeButton; TGCheckButtonView *_checkButton; bool _ignoreSetSelected; TGMediaPickerPhotoCounterButton *_photoCounterButton; @@ -114,6 +115,8 @@ @interface TGMediaPickerGalleryInterfaceView () id _context; bool _ignoreSelectionUpdates; + bool _canSendTelescope; + bool _canShowTelescope; } @property (nonatomic, strong) ASHandle *actionHandle; @@ -125,7 +128,7 @@ @implementation TGMediaPickerGalleryInterfaceView @synthesize safeAreaInset = _safeAreaInset; -- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages +- (instancetype)initWithContext:(id)context focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext stickersContext:(id)stickersContext hasSelectionPanel:(bool)hasSelectionPanel hasCameraButton:(bool)hasCameraButton recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages canShowTelescope:(bool)canShowTelescope canSendTelescope:(bool)canSendTelescope { self = [super initWithFrame:CGRectZero]; if (self != nil) @@ -192,6 +195,9 @@ - (instancetype)initWithContext:(id)context focusItem:( [[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"]; }; + _canSendTelescope = canSendTelescope; + _canShowTelescope = canShowTelescope; + _muteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 39.0f, 39.0f)]; _muteButton.hidden = true; _muteButton.adjustsImageWhenHighlighted = false; @@ -200,7 +206,22 @@ - (instancetype)initWithContext:(id)context focusItem:( [_muteButton setImage:[TGPhotoEditorInterfaceAssets muteActiveIcon] forState:UIControlStateSelected]; [_muteButton setImage:[TGPhotoEditorInterfaceAssets muteActiveIcon] forState:UIControlStateSelected | UIControlStateHighlighted]; [_muteButton addTarget:self action:@selector(toggleSendAsGif) forControlEvents:UIControlEventTouchUpInside]; - [_wrapperView addSubview:_muteButton]; + [_wrapperView addSubview:_muteButton]; + + _telescopeButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 39.0f, 39.0f)]; + _telescopeButton.hidden = true; + _telescopeButton.adjustsImageWhenHighlighted = false; + [_telescopeButton setBackgroundImage:[TGPhotoEditorInterfaceAssets gifBackgroundImage] forState:UIControlStateNormal]; + [_telescopeButton setImage:[TGPhotoEditorInterfaceAssets telescopeIcon] forState:UIControlStateNormal]; + if (_canSendTelescope) { + [_telescopeButton setImage:[TGPhotoEditorInterfaceAssets telescopeActiveIcon] forState:UIControlStateSelected]; + [_telescopeButton setImage:[TGPhotoEditorInterfaceAssets telescopeActiveIcon] forState:UIControlStateSelected | UIControlStateHighlighted]; + [_telescopeButton setImage:[TGPhotoEditorInterfaceAssets telescopeActiveIcon] forState:UIControlStateSelected]; + } else { + _telescopeButton.fadeDisabled = true; + } + [_telescopeButton addTarget:self action:@selector(toggleSendAsTelescope) forControlEvents:UIControlEventTouchUpInside]; + [_wrapperView addSubview:_telescopeButton]; if (recipientName.length > 0) { @@ -445,7 +466,8 @@ - (bool)updateGroupingButtonVisibility } id adjustments = [_editingContext adjustmentsForItem:item]; - if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]] && ((TGMediaVideoEditAdjustments *)adjustments).sendAsGif) + if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]] && (((TGMediaVideoEditAdjustments *)adjustments).sendAsGif || ((TGMediaVideoEditAdjustments *)adjustments).sendAsTelescope)) + { onlyGroupableMedia = false; break; @@ -614,6 +636,9 @@ - (void)itemFocused:(id)item itemView:(TGModernGalleryItemV } } strongSelf->_muteButton.hidden = !sendableAsGif; + if (strongSelf->_canShowTelescope) { + strongSelf->_telescopeButton.hidden = !sendableAsGif; + } } } file:__FILE_NAME__ line:__LINE__]]; @@ -887,6 +912,7 @@ - (void)updateEditorButtonsForAdjustments:(id)adjustment TGPhotoEditorTab disabledButtons = TGPhotoEditorNoneTab; _muteButton.selected = adjustments.sendAsGif; + _telescopeButton.selected = adjustments.sendAsTelescope; TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; if (qualityButton != nil) @@ -951,7 +977,7 @@ - (void)updateEditorButtonsForAdjustments:(id)adjustment }); } - if (adjustments.sendAsGif) + if (adjustments.sendAsGif || adjustments.sendAsTelescope) disabledButtons |= TGPhotoEditorQualityTab; [_portraitToolbarView setEditButtonsHighlighted:highlightedButtons]; @@ -1080,6 +1106,7 @@ - (void)setSelectionInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused { _checkButton.alpha = alpha; _muteButton.alpha = alpha; + _telescopeButton.alpha = alpha; _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; } completion:^(BOOL finished) @@ -1088,6 +1115,7 @@ - (void)setSelectionInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused { _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; + _telescopeButton.userInteractionEnabled = !hidden; } }]; @@ -1109,7 +1137,10 @@ - (void)setSelectionInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused _checkButton.userInteractionEnabled = !hidden; _muteButton.alpha = alpha; - _muteButton.userInteractionEnabled = !hidden; + _muteButton.userInteractionEnabled = !hidden; + + _telescopeButton.alpha = alpha; + _telescopeButton.userInteractionEnabled = !hidden; _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; @@ -1136,6 +1167,7 @@ - (void)setAllInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused delay { _checkButton.alpha = alpha; _muteButton.alpha = alpha; + _telescopeButton.alpha = alpha; _arrowView.alpha = alpha * 0.6; _recipientLabel.alpha = alpha; _portraitToolbarView.alpha = alpha; @@ -1148,6 +1180,7 @@ - (void)setAllInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused delay { _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; + _telescopeButton.userInteractionEnabled = !hidden; _portraitToolbarView.userInteractionEnabled = !hidden; _landscapeToolbarView.userInteractionEnabled = !hidden; _captionMixin.inputPanelView.userInteractionEnabled = !hidden; @@ -1173,7 +1206,10 @@ - (void)setAllInterfaceHidden:(bool)hidden delay:(NSTimeInterval)__unused delay _checkButton.userInteractionEnabled = !hidden; _muteButton.alpha = alpha; - _muteButton.userInteractionEnabled = !hidden; + _muteButton.userInteractionEnabled = !hidden; + + _telescopeButton.alpha = alpha; + _telescopeButton.userInteractionEnabled = !hidden; _arrowView.alpha = alpha * 0.6; _recipientLabel.alpha = alpha; @@ -1253,6 +1289,19 @@ - (void)toggleSendAsGif [(TGMediaPickerGalleryVideoItemView *)currentItemView toggleSendAsGif]; } +- (void)toggleSendAsTelescope +{ + if (![_currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)]) + return; + + TGModernGalleryItemView *currentItemView = _currentItemView; + bool sendableAsTelescope = [currentItemView isKindOfClass:[TGMediaPickerGalleryVideoItemView class]]; + if (sendableAsTelescope) + [(TGMediaPickerGalleryVideoItemView *)currentItemView toggleSendAsTelescope:_canSendTelescope dismissParent:^{ + [self cancelButtonPressed]; + }]; +} + - (void)toggleGrouping { [_selectionContext toggleGrouping]; @@ -1411,6 +1460,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event if (view == _photoCounterButton || view == _checkButton || view == _muteButton + || view == _telescopeButton || view == _groupButton || view == _cameraButton || [view isDescendantOfView:_headerWrapperView] @@ -1481,6 +1531,37 @@ - (CGRect)_muteButtonFrameForOrientation:(UIInterfaceOrientation)orientation scr return frame; } + +- (CGRect)_telescopeButtonFrameForOrientation:(UIInterfaceOrientation)orientation screenEdges:(UIEdgeInsets)screenEdges hasHeaderView:(bool)hasHeaderView +{ + CGRect frame = CGRectZero; + if (_safeAreaInset.top > 20.0f) + screenEdges.top += _safeAreaInset.top; + screenEdges.left += _safeAreaInset.left; + screenEdges.right -= _safeAreaInset.right; + + CGFloat panelInset = 0.0f; + + CGRect muteButtonFrame = [self _muteButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:hasHeaderView]; + + switch (orientation) + { + case UIInterfaceOrientationLandscapeLeft: + frame = CGRectMake(screenEdges.right - 47, muteButtonFrame.origin.y - muteButtonFrame.size.height - 5, _telescopeButton.frame.size.width, _telescopeButton.frame.size.height); + break; + + case UIInterfaceOrientationLandscapeRight: + frame = CGRectMake(screenEdges.left + 5, muteButtonFrame.origin.y - muteButtonFrame.size.height - 5, _telescopeButton.frame.size.width, _telescopeButton.frame.size.height); + break; + + default: + frame = CGRectMake(muteButtonFrame.origin.x + muteButtonFrame.size.width + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _telescopeButton.frame.size.width, _telescopeButton.frame.size.height); + break; + } + + return frame; +} + - (CGRect)_groupButtonFrameForOrientation:(UIInterfaceOrientation)orientation screenEdges:(UIEdgeInsets)screenEdges hasHeaderView:(bool)hasHeaderView { CGRect frame = CGRectZero; @@ -1746,6 +1827,7 @@ - (void)layoutSubviews } _muteButton.frame = [self _muteButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:true]; + _telescopeButton.frame = [self _telescopeButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:true]; _checkButton.frame = [self _checkButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:hasHeaderView]; _groupButton.frame = [self _groupButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:hasHeaderView]; [UIView performWithoutAnimation:^ diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index 1f41d51c92e..c80ca4845cb 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -40,6 +40,8 @@ @interface TGMediaPickerGalleryModel () NSString *_recipientName; bool _hasCamera; bool _isScheduledMessages; + bool _canShowTelescope; + bool _canSendTelescope; } @property (nonatomic, weak) TGPhotoEditorController *editorController; @@ -48,7 +50,7 @@ @interface TGMediaPickerGalleryModel () @implementation TGMediaPickerGalleryModel -- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages +- (instancetype)initWithContext:(id)context items:(NSArray *)items focusItem:(id)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName isScheduledMessages:(bool)isScheduledMessages canShowTelescope:(bool)canShowTelescope canSendTelescope:(bool)canSendTelescope { self = [super init]; if (self != nil) @@ -70,6 +72,8 @@ - (instancetype)initWithContext:(id)context items:(NSAr _recipientName = recipientName; _hasCamera = hasCamera; _isScheduledMessages = isScheduledMessages; + _canSendTelescope = canSendTelescope; + _canShowTelescope = canShowTelescope; __weak TGMediaPickerGalleryModel *weakSelf = self; if (selectionContext != nil) @@ -179,7 +183,7 @@ - (void)setCurrentItemWithIndex:(NSUInteger)index if (_interfaceView == nil) { __weak TGMediaPickerGalleryModel *weakSelf = self; - _interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName isScheduledMessages:_isScheduledMessages]; + _interfaceView = [[TGMediaPickerGalleryInterfaceView alloc] initWithContext:_context focusItem:_initialFocusItem selectionContext:_selectionContext editingContext:_editingContext stickersContext:_stickersContext hasSelectionPanel:_hasSelectionPanel hasCameraButton:_hasCamera recipientName:_recipientName isScheduledMessages:_isScheduledMessages canShowTelescope:_canShowTelescope canSendTelescope:_canSendTelescope]; _interfaceView.hasCaptions = _hasCaptions; _interfaceView.allowCaptionEntities = _allowCaptionEntities; _interfaceView.hasTimer = _hasTimer; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m index 7b6117cf929..745f1dc1c32 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m @@ -416,7 +416,7 @@ - (void)toggleSendAsGif if (cropRect.size.width < FLT_EPSILON) cropRect = CGRectMake(0.0f, 0.0f, originalSize.width, originalSize.height); - PGPhotoEditorValues *updatedAdjustments = [PGPhotoEditorValues editorValuesWithOriginalSize:originalSize cropRect:cropRect cropRotation:adjustments.cropRotation cropOrientation:adjustments.cropOrientation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:!adjustments.sendAsGif]; + PGPhotoEditorValues *updatedAdjustments = [PGPhotoEditorValues editorValuesWithOriginalSize:originalSize cropRect:cropRect cropRotation:adjustments.cropRotation cropOrientation:adjustments.cropOrientation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:!adjustments.sendAsGif sendAsTelescope:adjustments.sendAsTelescope]; [self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem]; bool sendAsGif = !adjustments.sendAsGif; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 72127e4ac48..2e0f0f6ed66 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -113,6 +113,7 @@ @interface TGMediaPickerGalleryVideoItemView() baseAdjustments = [strongSelf.item.editingContext adjustmentsForItem:strongSelf.item.editableMediaItem]; strongSelf->_sendAsGif = baseAdjustments.sendAsGif; + strongSelf->_sendAsTelescope = baseAdjustments.sendAsTelescope; [strongSelf _mutePlayer:baseAdjustments.sendAsGif]; if (baseAdjustments.sendAsGif || ([strongSelf itemIsLivePhoto])) @@ -1483,7 +1485,8 @@ - (void)toggleSendAsGif } bool sendAsGif = !adjustments.sendAsGif; - TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:adjustments.cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:sendAsGif preset:adjustments.preset]; + + TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:adjustments.cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:sendAsGif sendAsTelescope:false preset:adjustments.preset]; [self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem]; [_editableItemVariable set:[SSignal single:[self editableMediaItem]]]; @@ -1517,6 +1520,90 @@ - (void)toggleSendAsGif [self _mutePlayer:sendAsGif]; } +- (void)toggleSendAsTelescope:(bool)canSendAsTelescope dismissParent:(CompletionBlock)dismissParent; +{ + TGVideoEditAdjustments *adjustments = (TGVideoEditAdjustments *)[self.item.editingContext adjustmentsForItem:self.item.editableMediaItem]; + CGSize videoFrameSize = _videoDimensions; + CGRect cropRect = CGRectMake(0, 0, videoFrameSize.width, videoFrameSize.height); + NSTimeInterval trimStartValue = 0.0; + NSTimeInterval trimEndValue = _videoDuration; + if (adjustments != nil) + { + videoFrameSize = adjustments.cropRect.size; + cropRect = adjustments.cropRect; + + if (fabs(adjustments.trimEndValue - adjustments.trimStartValue) > DBL_EPSILON) + { + trimStartValue = adjustments.trimStartValue; + trimEndValue = adjustments.trimEndValue; + } + } + + bool sendAsTelescope = !adjustments.sendAsTelescope; + if (canSendAsTelescope) { + TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:adjustments.cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:false sendAsTelescope:sendAsTelescope preset:adjustments.preset]; + [self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem]; + + [_editableItemVariable set:[SSignal single:[self editableMediaItem]]]; + } + + if (sendAsTelescope) + { + UIView *parentView = [self.delegate itemViewDidRequestInterfaceView:self]; + if (!canSendAsTelescope) { + UIViewController *parentViewController = [self.delegate parentControllerForPresentation]; + if (parentViewController) { + // Define the URL + NSURL *url = [NSURL URLWithString:@"sg://resolve?domain=TelescopyBot&start=sgconvertdemo"]; + // Create UIAlertController + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Convert in @TelescopyBot" message:@"by Swiftgram" preferredStyle:UIAlertControllerStyleAlert]; + // Add an OK action with a handler to open the URL and then dismiss the parent view controller + UIAlertAction *okAction = [UIAlertAction actionWithTitle:TGLocalized(@"WebApp.OpenBot") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + // Check if the URL can be opened + if ([[UIApplication sharedApplication] canOpenURL:url]) { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + } + + if (dismissParent) { + dismissParent(); + } + }]; + [alertController addAction:okAction]; + // Add a Cancel action + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:TGLocalized(@"Common.Cancel") style:UIAlertActionStyleCancel handler:nil]; + [alertController addAction:cancelAction]; + // Present the alertController + [parentViewController presentViewController:alertController animated:YES completion:nil]; + } + + return; + } + + if (UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation])) + { + _tooltipContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, parentView.frame.size.width, parentView.frame.size.height)]; + [parentView addSubview:_tooltipContainerView]; + + NSMutableArray *actions = [[NSMutableArray alloc] init]; + NSString *text = @"Send as Round Video (Telescope),\n60 seconds limit."; + [actions addObject:@{@"title":text}]; + _tooltipContainerView.menuView.forceArrowOnTop = false; + _tooltipContainerView.menuView.multiline = true; + [_tooltipContainerView.menuView setButtonsAndActions:actions watcherHandle:nil]; + _tooltipContainerView.menuView.buttonHighlightDisabled = true; + [_tooltipContainerView.menuView sizeToFit]; + + CGRect iconViewFrame = CGRectMake(12 * 4 + 5, self.frame.size.height - 192.0 - _safeAreaInset.bottom, 40, 40); + [_tooltipContainerView showMenuFromRect:iconViewFrame animated:false]; + } + + if (!self.isPlaying) + [self play]; + } + + [self _mutePlayer:false]; +} + - (void)_mutePlayer:(bool)mute { if (iosMajorVersion() >= 7) @@ -1576,7 +1663,7 @@ - (void)updateEditAdjusments UIImageOrientation cropOrientation = (adjustments != nil) ? adjustments.cropOrientation : UIImageOrientationUp; CGFloat cropLockedAspectRatio = (adjustments != nil) ? adjustments.cropLockedAspectRatio : 0.0f; - TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:_scrubberView.trimStartValue trimEndValue:_scrubberView.trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:adjustments.sendAsGif preset:adjustments.preset]; + TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:_scrubberView.trimStartValue trimEndValue:_scrubberView.trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:adjustments.sendAsGif sendAsTelescope:adjustments.sendAsTelescope preset:adjustments.preset]; [self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem]; } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index 40b4c39f7a2..0ba0ba0efa9 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -84,7 +84,7 @@ - (instancetype)initWithContext:(id)context item:(id)it NSArray *galleryItems = [self prepareGalleryItemsForFetchResult:fetchResult selectionContext:selectionContext editingContext:editingContext stickersContext:stickersContext asFile:asFile enumerationBlock:enumerationBlock]; - TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName isScheduledMessages:false]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:[_windowManager context] items:galleryItems focusItem:focusItem selectionContext:selectionContext editingContext:editingContext hasCaptions:hasCaptions allowCaptionEntities:allowCaptionEntities hasTimer:hasTimer onlyCrop:onlyCrop inhibitDocumentCaptions:inhibitDocumentCaptions hasSelectionPanel:true hasCamera:false recipientName:recipientName isScheduledMessages:false canShowTelescope:false canSendTelescope:false]; _galleryModel = model; model.stickersContext = stickersContext; model.inhibitMute = inhibitMute; diff --git a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m index 15189ec41f5..de36e73f7d6 100644 --- a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m +++ b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m @@ -131,7 +131,7 @@ + (SSignal *)convertAVAsset:(AVAsset *)avAsset adjustments:(TGMediaVideoEditAdju CGSize dimensions = [avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject.naturalSize; TGMediaVideoConversionPreset preset = adjustments.sendAsGif ? TGMediaVideoConversionPresetAnimation : [self presetFromAdjustments:adjustments]; - if (!CGSizeEqualToSize(dimensions, CGSizeZero) && preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetVideoMessage && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileLow && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh && preset != TGMediaVideoConversionPresetPassthrough) + if (!CGSizeEqualToSize(dimensions, CGSizeZero) && preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetVideoMessage && preset != TGMediaVideoConversionPresetVideoMessageHD && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileLow && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh && preset != TGMediaVideoConversionPresetPassthrough) { TGMediaVideoConversionPreset bestPreset = [self bestAvailablePresetForDimensions:dimensions]; if (preset > bestPreset) @@ -1276,6 +1276,9 @@ + (CGSize)maximumSizeForPreset:(TGMediaVideoConversionPreset)preset case TGMediaVideoConversionPresetVideoMessage: return (CGSize){ 384.0f, 384.0f }; + case TGMediaVideoConversionPresetVideoMessageHD: + return (CGSize){ 384.0f, 384.0f }; + case TGMediaVideoConversionPresetProfileLow: return (CGSize){ 720.0f, 720.0f }; @@ -1414,6 +1417,9 @@ + (NSInteger)_videoBitrateKbpsForPreset:(TGMediaVideoConversionPreset)preset case TGMediaVideoConversionPresetVideoMessage: return 1000; + + case TGMediaVideoConversionPresetVideoMessageHD: + return 2000; case TGMediaVideoConversionPresetProfile: return 1500; @@ -1453,6 +1459,9 @@ + (NSInteger)_audioBitrateKbpsForPreset:(TGMediaVideoConversionPreset)preset case TGMediaVideoConversionPresetVideoMessage: return 64; + + case TGMediaVideoConversionPresetVideoMessageHD: + return 0; case TGMediaVideoConversionPresetAnimation: case TGMediaVideoConversionPresetProfile: diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m index 778a56f6c63..a15cb9b2945 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m @@ -145,6 +145,16 @@ + (UIImage *)muteActiveIcon return TGTintedImage([self gifIcon], [self accentColor]); } ++ (UIImage *)telescopeIcon +{ + return TGComponentsImageNamed(@"RecordVideoIconOverlay@2x.png"); +} + ++ (UIImage *)telescopeActiveIcon +{ + return TGTintedImage(TGTintedImage([self telescopeIcon], [self toolbarIconColor]), [self accentColor]); +} + + (UIImage *)gifIcon { return TGTintedImage([UIImage imageNamed:@"Editor/Gif"], [self toolbarIconColor]); diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 386f2749f5e..c3296b0d857 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -178,7 +178,7 @@ + (void)presentWithContext:(id)context controller:(TGVi galleryItem.editingContext = editingContext; galleryItem.stickersContext = stickersContext; - TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false canShowTelescope:false canSendTelescope:false]; model.controller = galleryController; model.stickersContext = stickersContext; @@ -297,7 +297,7 @@ + (void)presentEditorWithContext:(id)context controller } else { toolValues = @{}; } - PGPhotoEditorValues *editorValues = [PGPhotoEditorValues editorValuesWithOriginalSize:item.originalSize cropRect:cropRect cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0 cropMirrored:false toolValues:toolValues paintingData:nil sendAsGif:false]; + PGPhotoEditorValues *editorValues = [PGPhotoEditorValues editorValuesWithOriginalSize:item.originalSize cropRect:cropRect cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0 cropMirrored:false toolValues:toolValues paintingData:nil sendAsGif:false sendAsTelescope:false]; TGPhotoEditorController *editorController = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:item intent:TGPhotoEditorControllerWallpaperIntent adjustments:editorValues caption:nil screenImage:thumbnailImage availableTabs:TGPhotoEditorToolsTab selectedTab:TGPhotoEditorToolsTab]; editorController.editingContext = editingContext; diff --git a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m index ffa4c1eb682..7d5b2aeba3a 100644 --- a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m +++ b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m @@ -14,6 +14,7 @@ const NSTimeInterval TGVideoEditMinimumTrimmableDuration = 1.5; const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; +const NSTimeInterval TGVideoEditMaximumTelescopeDuration = 60; @implementation TGVideoEditAdjustments @@ -25,6 +26,7 @@ @implementation TGVideoEditAdjustments @synthesize cropMirrored = _cropMirrored; @synthesize paintingData = _paintingData; @synthesize sendAsGif = _sendAsGif; +@synthesize sendAsTelescope = _sendAsTelescope; @synthesize toolValues = _toolValues; + (instancetype)editAdjustmentsWithOriginalSize:(CGSize)originalSize @@ -38,6 +40,7 @@ + (instancetype)editAdjustmentsWithOriginalSize:(CGSize)originalSize toolValues:(NSDictionary *)toolValues paintingData:(TGPaintingData *)paintingData sendAsGif:(bool)sendAsGif + sendAsTelescope:(bool)sendAsTelescope preset:(TGMediaVideoConversionPreset)preset { TGVideoEditAdjustments *adjustments = [[[self class] alloc] init]; @@ -52,6 +55,7 @@ + (instancetype)editAdjustmentsWithOriginalSize:(CGSize)originalSize adjustments->_toolValues = toolValues; adjustments->_paintingData = paintingData; adjustments->_sendAsGif = sendAsGif; + adjustments->_sendAsTelescope = sendAsTelescope; adjustments->_preset = preset; if (trimStartValue > trimEndValue) @@ -86,6 +90,8 @@ + (instancetype)editAdjustmentsWithDictionary:(NSDictionary *)dictionary } if (dictionary[@"sendAsGif"]) adjustments->_sendAsGif = [dictionary[@"sendAsGif"] boolValue]; + if (dictionary[@"sendAsTelescope"]) + adjustments->_sendAsTelescope = [dictionary[@"sendAsTelescope"] boolValue]; if (dictionary[@"preset"]) adjustments->_preset = (TGMediaVideoConversionPreset)[dictionary[@"preset"] integerValue]; if (dictionary[@"tools"]) { @@ -122,6 +128,8 @@ + (instancetype)editAdjustmentsWithOriginalSize:(CGSize)originalSize adjustments->_preset = preset; if (preset == TGMediaVideoConversionPresetAnimation) adjustments->_sendAsGif = true; + if (preset == TGMediaVideoConversionPresetVideoMessage || preset == TGMediaVideoConversionPresetVideoMessageHD) + adjustments->_sendAsTelescope = true; return adjustments; } @@ -140,6 +148,7 @@ + (instancetype)editAdjustmentsWithPhotoEditorValues:(PGPhotoEditorValues *)valu adjustments->_cropMirrored = values.cropMirrored; adjustments->_paintingData = [values.paintingData dataForAnimation]; adjustments->_sendAsGif = true; + adjustments->_sendAsTelescope = values.sendAsTelescope; adjustments->_preset = preset; return adjustments; @@ -159,6 +168,7 @@ + (instancetype)editAdjustmentsWithPhotoEditorValues:(PGPhotoEditorValues *)valu adjustments->_cropMirrored = values.cropMirrored; adjustments->_paintingData = [values.paintingData dataForAnimation]; adjustments->_sendAsGif = true; + adjustments->_sendAsTelescope = values.sendAsTelescope; adjustments->_preset = preset; adjustments->_documentId = documentId; adjustments->_colors = colors; @@ -180,6 +190,7 @@ + (instancetype)editAdjustmentsWithPhotoEditorValues:(PGPhotoEditorValues *)valu adjustments->_cropMirrored = values.cropMirrored; adjustments->_paintingData = [values.paintingData dataForAnimation]; adjustments->_sendAsGif = true; + adjustments->_sendAsTelescope = values.sendAsTelescope; adjustments->_preset = preset; adjustments->_stickerPackId = stickerPackId; adjustments->_stickerPackAccessHash = stickerPackAccessHash; @@ -205,6 +216,7 @@ - (instancetype)editAdjustmentsWithPreset:(TGMediaVideoConversionPreset)preset m adjustments->_toolValues = _toolValues; adjustments->_videoStartValue = _videoStartValue; adjustments->_sendAsGif = preset == TGMediaVideoConversionPresetAnimation ? true : _sendAsGif; + adjustments->_sendAsTelescope = (preset == TGMediaVideoConversionPresetVideoMessage || preset == TGMediaVideoConversionPresetVideoMessageHD) ? true : _sendAsTelescope; if (maxDuration > DBL_EPSILON) { @@ -235,6 +247,7 @@ - (instancetype)editAdjustmentsWithPreset:(TGMediaVideoConversionPreset)preset v adjustments->_trimEndValue = trimEndValue; adjustments->_paintingData = _paintingData; adjustments->_sendAsGif = _sendAsGif; + adjustments->_sendAsTelescope = _sendAsTelescope; adjustments->_preset = preset; adjustments->_toolValues = _toolValues; adjustments->_videoStartValue = videoStartValue; @@ -285,6 +298,7 @@ - (NSDictionary *)dictionary } dict[@"sendAsGif"] = @(self.sendAsGif); + dict[@"sendAsTelescope"] = @(self.sendAsTelescope); if (self.preset != TGMediaVideoConversionPresetCompressedDefault) dict[@"preset"] = @(self.preset); @@ -463,6 +477,9 @@ - (BOOL)isEqual:(id)object if (self.sendAsGif != adjustments.sendAsGif) return false; + + if (self.sendAsTelescope != adjustments.sendAsTelescope) + return false; return true; } diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index d05b594a219..6f4825687ab 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -66,6 +66,7 @@ @interface TGVideoMessageCaptureController () )context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder +# pragma mark - Swiftgram +- (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder startWithRearCam:(bool)startWithRearCam { self = [super initWithContext:context]; if (self != nil) @@ -173,7 +174,13 @@ - (instancetype)initWithContext:(id)context forStory:(b _queue = [[SQueue alloc] init]; _previousDuration = 0.0; - _preferredPosition = AVCaptureDevicePositionFront; +#pragma mark - Swiftgram + if (startWithRearCam) { + _preferredPosition = AVCaptureDevicePositionBack; + } else { + _preferredPosition = AVCaptureDevicePositionFront; + } + _startWithRearCam = startWithRearCam; self.isImportant = true; _controlsFrame = controlsFrame; @@ -1060,7 +1067,7 @@ - (void)finishWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTim CGFloat minSize = MIN(thumbnailImage.size.width, thumbnailImage.size.height); CGFloat maxSize = MAX(thumbnailImage.size.width, thumbnailImage.size.height); - bool mirrored = true; + bool mirrored = !_startWithRearCam; UIImageOrientation orientation = [self orientationForThumbnailWithTransform:_capturePipeline.videoTransform mirrored:mirrored]; UIImage *image = TGPhotoEditorCrop(thumbnailImage, nil, orientation, 0.0f, CGRectMake((maxSize - minSize) / 2.0f, 0.0f, minSize, minSize), mirrored, CGSizeMake(240.0f, 240.0f), thumbnailImage.size, true); @@ -1079,7 +1086,7 @@ - (void)finishWithURL:(NSURL *)url dimensions:(CGSize)dimensions duration:(NSTim if (trimStartValue > DBL_EPSILON || trimEndValue < _duration - DBL_EPSILON) { - adjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:dimensions cropRect:CGRectMake(0.0f, 0.0f, dimensions.width, dimensions.height) cropOrientation:UIImageOrientationUp cropRotation:0.0 cropLockedAspectRatio:1.0 cropMirrored:false trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:nil paintingData:nil sendAsGif:false preset:TGMediaVideoConversionPresetVideoMessage]; + adjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:dimensions cropRect:CGRectMake(0.0f, 0.0f, dimensions.width, dimensions.height) cropOrientation:UIImageOrientationUp cropRotation:0.0 cropLockedAspectRatio:1.0 cropMirrored:false trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:nil paintingData:nil sendAsGif:false sendAsTelescope:false preset:TGMediaVideoConversionPresetVideoMessage]; duration = trimEndValue - trimStartValue; } diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index b919be5465a..ce6dcff59b1 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "LegacyMediaPickerUI", module_name = "LegacyMediaPickerUI", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 86c841110fd..73ef4fe3857 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import LegacyComponents @@ -396,7 +397,8 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A defer { TempBox.shared.dispose(tempFile) } - if let scaledImageData = compressImageToJPEG(scaledImage, quality: 0.6, tempFilePath: tempFile.path) { + // MARK: Swiftgram + if let scaledImageData = compressImageToJPEG(scaledImage, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) { let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) @@ -757,6 +759,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A break } case let .video(data, thumbnail, adjustments, caption, asFile, asAnimation, stickers): + var adjustments = adjustments var finalDimensions: CGSize var finalDuration: Double switch data { @@ -811,8 +814,77 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A preset = TGMediaVideoConversionPresetAnimation } - if !asAnimation { - finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: TGMediaVideoConversionPresetCompressedMedium) + // MARK: Swiftgram + // TODO(swiftgram): Nice thumbnail + var asTelescope = false + if let strongAdjustments = adjustments, strongAdjustments.sendAsTelescope { + asTelescope = true + // Final size + let size = CGSize(width: finalDimensions.width, height: finalDimensions.height) + + // Respecting user's crop + var cropRect = strongAdjustments.cropRect + + let originalSize: CGSize + if strongAdjustments.cropApplied(forAvatar: false) { + originalSize = strongAdjustments.originalSize + } else { + // It's a hack, video is resized according to the quality preset + // To prevent this resize we must set original size the same as the after-resized video + originalSize = size + } + + // Already square + if abs(finalDimensions.width - finalDimensions.height) < CGFloat.ulpOfOne { + cropRect = cropRect.insetBy(dx: 13.0, dy: 13.0) + cropRect = cropRect.offsetBy(dx: 2.0, dy: 3.0) + } else { + // Need to make a square + let shortestSide = min(size.width, size.height) + let newX = cropRect.origin.x + (size.width - shortestSide) / 2.0 + let newY = cropRect.origin.y + (size.height - shortestSide) / 2.0 + cropRect = CGRect(x: newX, y: newY, width: shortestSide, height: shortestSide) + print("size.width \(size.width)") + print("size.height \(size.height)") + print("shortestSide \(shortestSide)") + print("cropRect.origin.x \(cropRect.origin.x)") + print("cropRect.origin.y \(cropRect.origin.y)") + print("newX \(newX)") + print("newY \(newY)") + } + + let maxDuration: Double = 60.0 + let trimmedDuration: TimeInterval + if strongAdjustments.trimApplied() { + trimmedDuration = strongAdjustments.trimEndValue - strongAdjustments.trimStartValue + } else { + trimmedDuration = finalDuration + } + + let trimEndValueLimited: TimeInterval + if trimmedDuration > maxDuration { + trimEndValueLimited = strongAdjustments.trimEndValue - (trimmedDuration - maxDuration) + } else { + trimEndValueLimited = strongAdjustments.trimEndValue + } + + print("Preset TGMediaVideoConversionPresetVideoMessageHD \(TGMediaVideoConversionPresetVideoMessageHD)") + print("Preset TGMediaVideoConversionPresetVideoMessage \(TGMediaVideoConversionPresetVideoMessage)") + print("Preset TGMediaVideoConversionPresetCompressedLow \(TGMediaVideoConversionPresetCompressedLow)") + + // Dynamically calculate size with different presets and use the best one + for presetTest in [TGMediaVideoConversionPresetVideoMessageHD, TGMediaVideoConversionPresetVideoMessage, TGMediaVideoConversionPresetCompressedLow] { + adjustments = TGVideoEditAdjustments(originalSize: originalSize, cropRect: cropRect, cropOrientation: strongAdjustments.cropOrientation, cropRotation: strongAdjustments.cropRotation, cropLockedAspectRatio: 1.0, cropMirrored: strongAdjustments.cropMirrored, trimStartValue: strongAdjustments.trimStartValue, trimEndValue: trimEndValueLimited, toolValues: strongAdjustments.toolValues, paintingData: strongAdjustments.paintingData, sendAsGif: false, sendAsTelescope: strongAdjustments.sendAsTelescope, preset: presetTest) + + finalDimensions = TGMediaVideoConverter.dimensions(for: finalDimensions, adjustments: adjustments, preset: presetTest) + + let estimatedVideoMessageSize = TGMediaVideoConverter.estimatedSize(for: presetTest, duration: finalDuration, hasAudio: true) + if estimatedVideoMessageSize < 8 * 1024 * 1024 { + print("Using preset \(presetTest)") + preset = presetTest + break + } + } } var resourceAdjustments: VideoMediaResourceAdjustments? @@ -890,7 +962,10 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles)) fileAttributes.append(.HasLinkedStickers) } - + // MARK: Swiftgram + if asTelescope { + fileAttributes = [.FileName(fileName: "video.mp4"), .Video(duration: finalDuration, size: PixelDimensions(finalDimensions), flags: [.instantRoundVideo], preloadSize: nil, coverTime: nil, videoCodec: nil)] + } let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index f226d6ac75f..3d5125c42ca 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -808,7 +808,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } for attribute in message.attributes { - if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { + if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) != nil { isRestricted = true break } diff --git a/submodules/LocalMediaResources/BUILD b/submodules/LocalMediaResources/BUILD index b0f3f832fe5..636f28dfa85 100644 --- a/submodules/LocalMediaResources/BUILD +++ b/submodules/LocalMediaResources/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "LocalMediaResources", module_name = "LocalMediaResources", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", diff --git a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift index 0f2efd43722..4077c27fdfc 100644 --- a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift +++ b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Photos @@ -121,7 +122,8 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he if let width, let height { size = CGSize(width: CGFloat(width), height: CGFloat(height)) } else { - size = CGSize(width: 1280.0, height: 1280.0) + // MARK: Swiftgram + size = SGSimpleSettings.shared.sendLargePhotos ? CGSize(width: 2560.0, height: 2560.0) : CGSize(width: 1280.0, height: 1280.0) } var targetSize = PHImageManagerMaximumSize @@ -178,7 +180,7 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he defer { TempBox.shared.dispose(tempFile) } - if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: 0.6, tempFilePath: tempFile.path) { + if let scaledImage = scaledImage, let data = compressImageToJPEG(scaledImage, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) { #if DEBUG print("compression completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") #endif @@ -188,7 +190,7 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he subscriber.putCompletion() } case .jxl: - if let scaledImage = scaledImage, let data = compressImageToJPEGXL(scaledImage, quality: Int(quality ?? 75)) { + if let scaledImage = scaledImage, let data = compressImageToJPEGXL(scaledImage, quality: Int(SGSimpleSettings.shared.outgoingPhotoQuality)) { #if DEBUG print("jpegxl compression completion \((CACurrentMediaTime() - startTime) * 1000.0) ms") #endif diff --git a/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift b/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift index 243b6220daa..ca60a7e8bca 100644 --- a/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift +++ b/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift @@ -8,6 +8,7 @@ private struct TranscriptionResult { var text: String var confidence: Float var isFinal: Bool + var locale: String } private func transcribeAudio(path: String, locale: String) -> Signal { @@ -67,7 +68,7 @@ private func transcribeAudio(path: String, locale: String) -> Signal Signal { var signals: [Signal] = [] - var locales: [String] = [] - if !locales.contains(Locale.current.identifier) { - locales.append(Locale.current.identifier) - } - if locales.isEmpty { - locales.append("en-US") - } + let locales: [String] = [appLocale] + // Device can effectivelly transcribe only one language at a time. So it will be wise to run language recognition once for each popular language, check the confidence, start over with most confident language and output something it has already generated +// if !locales.contains(Locale.current.identifier) { +// locales.append(Locale.current.identifier) +// } +// if locales.isEmpty { +// locales.append("en-US") +// } + // Dictionary to hold accumulated transcriptions and confidences for each locale + var accumulatedTranscription: [String: (confidence: Float, text: [String])] = [:] for locale in locales { signals.append(transcribeAudio(path: path, locale: locale)) } - var resultSignal: Signal<[TranscriptionResult?], NoError> = .single([]) - for signal in signals { - resultSignal = resultSignal |> mapToSignal { result -> Signal<[TranscriptionResult?], NoError> in - return signal |> map { next in - return result + [next] + // We need to combine results per-language and compare their total confidence, (instead of outputting everything we have to the signal) + // return the one with the most confidence + let resultSignal: Signal<[TranscriptionResult?], NoError> = signals.reduce(.single([])) { (accumulator, signal) in + return accumulator + |> mapToSignal { results in + return signal + |> map { next in + return results + [next] + } } - } } + return resultSignal |> map { results -> LocallyTranscribedAudio? in - let sortedResults = results.compactMap({ $0 }).sorted(by: { lhs, rhs in - return lhs.confidence > rhs.confidence - }) - return sortedResults.first.flatMap { result -> LocallyTranscribedAudio in - return LocallyTranscribedAudio(text: result.text, isFinal: result.isFinal) + for result in results { + if let result = result { + var result = result + if result.text.isEmpty { + result.text = "..." + } + if var existing = accumulatedTranscription[result.locale] { + existing.text.append(result.text) + existing.confidence += result.confidence + accumulatedTranscription[result.locale] = existing + } else { + accumulatedTranscription[result.locale] = (result.confidence, [result.text]) + } + } + } + + // Find the locale with the highest accumulated confidence + guard let bestLocale = accumulatedTranscription.max(by: { $0.value.confidence < $1.value.confidence }) else { + return nil } + + let combinedText = bestLocale.value.text.joined(separator: ". ") + // Assume 'isFinal' is true if the last result in 'results' is final. Adjust if needed. + let isFinal = results.compactMap({ $0 }).last?.isFinal ?? false + return LocallyTranscribedAudio(text: combinedText, isFinal: isFinal) } + } diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index d1a9f17cae5..45f78d678da 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -134,8 +134,8 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, recipientName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } } - - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName, isScheduledMessages: isScheduledMessages)! + let currentAppConfiguration = context.currentAppConfiguration.with { $0 } + let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName, isScheduledMessages: isScheduledMessages, canShowTelescope: currentAppConfiguration.sgWebSettings.global.canShowTelescope, canSendTelescope: currentAppConfiguration.sgWebSettings.user.canSendTelescope)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 3fab1132595..140a86cb51d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -610,7 +611,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } }) - if let controller = self.controller, case .assets(nil, .default) = controller.subject { + if let controller = self.controller, case .assets(nil, .default) = controller.subject, !SGSimpleSettings.shared.disableGalleryCamera { let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)! diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index 318c28c7c54..a38b2e21591 100644 --- a/submodules/MediaPlayer/BUILD +++ b/submodules/MediaPlayer/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "UniversalMediaPlayer", module_name = "UniversalMediaPlayer", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/TelegramCore:TelegramCore", "//submodules/Postbox:Postbox", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h index c9b041c4724..437bc6bce0f 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTContext.h @@ -78,6 +78,7 @@ @property (nonatomic, strong, readonly) MTApiEnvironment * _Nonnull apiEnvironment; @property (nonatomic, readonly) bool isTestingEnvironment; @property (nonatomic, readonly) bool useTempAuthKeys; +@property (nonatomic, readonly) bool forceLocalDNS; @property (nonatomic) int32_t tempKeyExpiration; @property (nonatomic, copy) id _Nonnull (^ _Nullable makeTcpConnectionInterface)(id _Nonnull delegate, dispatch_queue_t _Nonnull delegateQueue); @@ -91,7 +92,7 @@ + (void)copyAuthInfoFrom:(id _Nonnull)keychain toTempKeychain:(id _Nonnull)tempKeychain; -- (instancetype _Nonnull)initWithSerialization:(id _Nonnull)serialization encryptionProvider:(id _Nonnull)encryptionProvider apiEnvironment:(MTApiEnvironment * _Nonnull)apiEnvironment isTestingEnvironment:(bool)isTestingEnvironment useTempAuthKeys:(bool)useTempAuthKeys; +- (instancetype _Nonnull)initWithSerialization:(id _Nonnull)serialization encryptionProvider:(id _Nonnull)encryptionProvider apiEnvironment:(MTApiEnvironment * _Nonnull)apiEnvironment isTestingEnvironment:(bool)isTestingEnvironment useTempAuthKeys:(bool)useTempAuthKeys forceLocalDNS:(bool)forceLocalDNS; - (void)performBatchUpdates:(void (^ _Nonnull)())block; diff --git a/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m b/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m index 68e2f921ed8..448a34e8d43 100644 --- a/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m +++ b/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m @@ -302,7 +302,7 @@ + (MTSignal *)fetchConfigFromAddress:(MTBackupDatacenterAddress *)address curren apiEnvironment.disableUpdates = true; apiEnvironment.langPack = currentContext.apiEnvironment.langPack; - MTContext *context = [[MTContext alloc] initWithSerialization:currentContext.serialization encryptionProvider:currentContext.encryptionProvider apiEnvironment:apiEnvironment isTestingEnvironment:currentContext.isTestingEnvironment useTempAuthKeys:false]; + MTContext *context = [[MTContext alloc] initWithSerialization:currentContext.serialization encryptionProvider:currentContext.encryptionProvider apiEnvironment:apiEnvironment isTestingEnvironment:currentContext.isTestingEnvironment useTempAuthKeys:false forceLocalDNS:currentContext.forceLocalDNS]; context.makeTcpConnectionInterface = currentContext.makeTcpConnectionInterface; diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index a86308e06ae..60e58ffd64a 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -230,7 +230,7 @@ - (instancetype)init return self; } -- (instancetype)initWithSerialization:(id)serialization encryptionProvider:(id)encryptionProvider apiEnvironment:(MTApiEnvironment *)apiEnvironment isTestingEnvironment:(bool)isTestingEnvironment useTempAuthKeys:(bool)useTempAuthKeys +- (instancetype)initWithSerialization:(id)serialization encryptionProvider:(id)encryptionProvider apiEnvironment:(MTApiEnvironment *)apiEnvironment isTestingEnvironment:(bool)isTestingEnvironment useTempAuthKeys:(bool)useTempAuthKeys forceLocalDNS:(bool)forceLocalDNS { NSAssert(serialization != nil, @"serialization should not be nil"); NSAssert(apiEnvironment != nil, @"apiEnvironment should not be nil"); @@ -246,6 +246,7 @@ - (instancetype)initWithSerialization:(id)serialization encrypt _apiEnvironment = apiEnvironment; _isTestingEnvironment = isTestingEnvironment; _useTempAuthKeys = useTempAuthKeys; + _forceLocalDNS = forceLocalDNS; _tempKeyExpiration = 24 * 60 * 60; diff --git a/submodules/MtProtoKit/Sources/MTProxyConnectivity.m b/submodules/MtProtoKit/Sources/MTProxyConnectivity.m index 80c1bef6d30..dcedab0de75 100644 --- a/submodules/MtProtoKit/Sources/MTProxyConnectivity.m +++ b/submodules/MtProtoKit/Sources/MTProxyConnectivity.m @@ -64,7 +64,7 @@ + (MTSignal *)pingWithAddress:(MTDatacenterAddress *)address datacenterId:(NSUIn MTPayloadData payloadData; NSData *data = [MTDiscoverConnectionSignals payloadData:&payloadData context:context address:address]; - MTContext *proxyContext = [[MTContext alloc] initWithSerialization:context.serialization encryptionProvider:context.encryptionProvider apiEnvironment:[[context apiEnvironment] withUpdatedSocksProxySettings:settings] isTestingEnvironment:context.isTestingEnvironment useTempAuthKeys:false]; + MTContext *proxyContext = [[MTContext alloc] initWithSerialization:context.serialization encryptionProvider:context.encryptionProvider apiEnvironment:[[context apiEnvironment] withUpdatedSocksProxySettings:settings] isTestingEnvironment:context.isTestingEnvironment useTempAuthKeys:false forceLocalDNS:context.forceLocalDNS]; proxyContext.makeTcpConnectionInterface = context.makeTcpConnectionInterface; diff --git a/submodules/MtProtoKit/Sources/MTTcpConnection.m b/submodules/MtProtoKit/Sources/MTTcpConnection.m index e8cf2398818..416fedc2d2c 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -760,6 +760,8 @@ @interface MTTcpConnection () NSMutableArray *_pendingDataQueue; NSMutableData *_receivedDataBuffer; MTTcpReceiveData *_pendingReceiveData; + + bool _forceLocalDNS; } @property (nonatomic) int64_t packetHeadDecodeToken; @@ -850,6 +852,8 @@ - (instancetype)initWithContext:(MTContext *)context datacenterId:(NSInteger)dat _pendingDataQueue = [[NSMutableArray alloc] init]; _receivedDataBuffer = [[NSMutableData alloc] init]; + + _forceLocalDNS = context.forceLocalDNS; } return self; } @@ -920,7 +924,7 @@ - (void)start if (isHostname) { int32_t port = _socksPort; - resolveSignal = [[MTDNS resolveHostnameUniversal:_socksIp port:port] map:^id(NSString *resolvedIp) { + resolveSignal = [( _forceLocalDNS ? [MTDNS resolveHostnameNative:_socksIp port:port] : [MTDNS resolveHostnameUniversal:_socksIp port:port]) map:^id(NSString *resolvedIp) { return [[MTTcpConnectionData alloc] initWithIp:resolvedIp port:port isSocks:true]; }]; } else { @@ -938,7 +942,7 @@ - (void)start if (isHostname) { int32_t port = _mtpPort; - resolveSignal = [[MTDNS resolveHostnameUniversal:_mtpIp port:port] map:^id(NSString *resolvedIp) { + resolveSignal = [( _forceLocalDNS ? [MTDNS resolveHostnameNative:_mtpIp port:port] : [MTDNS resolveHostnameUniversal:_mtpIp port:port]) map:^id(NSString *resolvedIp) { return [[MTTcpConnectionData alloc] initWithIp:resolvedIp port:port isSocks:false]; }]; } else { diff --git a/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift b/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift index 3d34a6ef966..85fc70ac00c 100644 --- a/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift +++ b/submodules/NotificationMuteSettingsUI/Sources/NotificationMuteSettingsController.swift @@ -42,9 +42,13 @@ public func notificationMuteSettingsController(presentationData: PresentationDat updateSettings(muteInterval) } + // MARK: Swiftgram let options: [NotificationMuteOption] = [ .enable, .interval(1 * 60 * 60), + .interval(4 * 60 * 60), + .interval(8 * 60 * 60), + .interval(1 * 24 * 60 * 60), .interval(2 * 24 * 60 * 60), .disable ] diff --git a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift index cdafe9a37c9..38d409501fc 100644 --- a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift +++ b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift @@ -8,6 +8,15 @@ public extension Message { } func restrictionReason(platform: String, contentSettings: ContentSettings) -> String? { + // MARK: Swiftgram + if let author = self.author { + let chatId = author.id.id._internalGetInt64Value() + if contentSettings.appConfiguration.sgWebSettings.global.forceReasons.contains(chatId) { + return "Unavailable in Swiftgram due to App Store Guidelines" + } else if contentSettings.appConfiguration.sgWebSettings.global.unforceReasons.contains(chatId) { + return nil + } + } if let attribute = self.restrictedContentAttribute { if let value = attribute.platformText(platform: platform, contentSettings: contentSettings) { return value @@ -18,17 +27,40 @@ public extension Message { } public extension RestrictedContentMessageAttribute { - func platformText(platform: String, contentSettings: ContentSettings) -> String? { + func platformText(platform: String, contentSettings: ContentSettings, chatId: Int64? = nil) -> String? { + // MARK: Swiftgram + if let chatId = chatId { + if contentSettings.appConfiguration.sgWebSettings.global.forceReasons.contains(chatId) { + return "Unavailable in Swiftgram due to App Store Guidelines" + } else if contentSettings.appConfiguration.sgWebSettings.global.unforceReasons.contains(chatId) { + return nil + } + } for rule in self.rules { if rule.reason == "sensitive" { continue } if rule.platform == "all" || rule.platform == "ios" || contentSettings.addContentRestrictionReasons.contains(rule.platform) { if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { - return rule.text + return rule.text + "\n" + "\(rule.reason)-\(rule.platform)" } } } return nil } } + +// MARK: Swiftgram +public extension Message { + func canRevealContent(contentSettings: ContentSettings) -> Bool { + if contentSettings.appConfiguration.sgWebSettings.global.canViewMessages && self.flags.contains(.CopyProtected) { + let messageContentWasUnblocked = self.restrictedContentAttribute != nil && self.isRestricted(platform: "ios", contentSettings: ContentSettings.default) && !self.isRestricted(platform: "ios", contentSettings: contentSettings) + var authorWasUnblocked: Bool = false + if let author = self.author { + authorWasUnblocked = author.restrictionText(platform: "ios", contentSettings: ContentSettings.default) != nil && author.restrictionText(platform: "ios", contentSettings: contentSettings) == nil + } + return messageContentWasUnblocked || authorWasUnblocked + } + return false + } +} diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 9391945d663..c20f14fc1f1 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -152,8 +152,8 @@ public final class Transaction { self.postbox?.deleteMessagesInRange(peerId: peerId, namespace: namespace, minId: minId, maxId: maxId, forEachMedia: forEachMedia) } - public func withAllMessages(peerId: PeerId, namespace: MessageId.Namespace? = nil, _ f: (Message) -> Bool) { - self.postbox?.withAllMessages(peerId: peerId, namespace: namespace, f) + public func withAllMessages(peerId: PeerId, namespace: MessageId.Namespace? = nil, reversed: Bool = false, _ f: (Message) -> Bool) { + self.postbox?.withAllMessages(peerId: peerId, namespace: namespace, reversed: reversed, f) } public func clearHistory(_ peerId: PeerId, threadId: Int64?, minTimestamp: Int32?, maxTimestamp: Int32?, namespaces: MessageIdNamespaces, forEachMedia: ((Media) -> Void)?) { @@ -2197,8 +2197,10 @@ final class PostboxImpl { self.messageHistoryTable.removeMessagesInRange(peerId: peerId, namespace: namespace, minId: minId, maxId: maxId, operationsByPeerId: &self.currentOperationsByPeerId, updatedMedia: &self.currentUpdatedMedia, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations, globalTagsOperations: &self.currentGlobalTagsOperations, pendingActionsOperations: &self.currentPendingMessageActionsOperations, updatedMessageActionsSummaries: &self.currentUpdatedMessageActionsSummaries, updatedMessageTagSummaries: &self.currentUpdatedMessageTagSummaries, invalidateMessageTagSummaries: &self.currentInvalidateMessageTagSummaries, localTagsOperations: &self.currentLocalTagsOperations, timestampBasedMessageAttributesOperations: &self.currentTimestampBasedMessageAttributesOperations, forEachMedia: forEachMedia) } - fileprivate func withAllMessages(peerId: PeerId, namespace: MessageId.Namespace?, _ f: (Message) -> Bool) { - for index in self.messageHistoryTable.allMessageIndices(peerId: peerId, namespace: namespace) { + fileprivate func withAllMessages(peerId: PeerId, namespace: MessageId.Namespace?, reversed: Bool = false, _ f: (Message) -> Bool) { + var indexes = self.messageHistoryTable.allMessageIndices(peerId: peerId, namespace: namespace) + if reversed { indexes.reverse() } + for index in indexes { if let message = self.messageHistoryTable.getMessage(index) { if !f(self.renderIntermediateMessage(message)) { break @@ -3562,6 +3564,10 @@ final class PostboxImpl { } chatPeerIds.append(contentsOf: additionalChatPeerIds) + if let peerId = self.searchLocalPeerId(query: query) { + chatPeerIds.append(peerId) + } + for peerId in chatPeerIds { if let peer = self.peerTable.get(peerId) { var peers = SimpleDictionary() @@ -4966,3 +4972,48 @@ public class Postbox { } } } + + +// MARK: Swiftgram +extension PostboxImpl { + func searchLocalPeerId(query: String) -> PeerId? { + var result: PeerId? = nil + let minus100Prefix = "-100" + var query = query + if query.hasPrefix(minus100Prefix) { + query = String(query.dropFirst(minus100Prefix.count)) + } + guard let queryInt64 = Int64(query) else { return nil } + let possiblePeerId = PeerId(queryInt64) + + + if self.cachedPeerDataTable.get(possiblePeerId) != nil { + #if DEBUG + print("Found peer \(queryInt64) in cachedPeerDataTable") + #endif + return possiblePeerId + } + + if self.peerTable.get(possiblePeerId) != nil { + #if DEBUG + print("Found peer \(queryInt64) in peerTable") + #endif + return possiblePeerId + } + + self.valueBox.scanInt64(self.chatListIndexTable.table, keys: { key in + let peerId = PeerId(key) + let peerIdInt64 = peerId.id._internalGetInt64Value() + if queryInt64 == peerIdInt64 /* /* For basic groups */ || abs(queryInt64) == peerIdInt64 */ { + #if DEBUG + print("Found peer \(queryInt64) in chatListIndexTable") + #endif + result = peerId + return false + } + return true + }) + + return result + } +} diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 9cad24de432..b57ec3f5b35 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -48,6 +48,10 @@ filegroup( visibility = ["//visibility:public"], ) +sgdeps = [ + "//Swiftgram/SGStrings:SGStrings" +] + swift_library( name = "PremiumUI", module_name = "PremiumUI", @@ -60,7 +64,7 @@ swift_library( data = [ ":PremiumUIBundle", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 50bea98f1ae..a545339cf34 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -2407,7 +2407,8 @@ public class PremiumBoostLevelsScreen: ViewController { title: presentationData.strings.ChannelBoost_MoreBoosts_Title, text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { [weak controller] in + // MARK: Swiftgram + /*TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { [weak controller] in if let navigationController = controller?.navigationController { controller?.dismiss(animated: true, completion: nil) @@ -2416,7 +2417,7 @@ public class PremiumBoostLevelsScreen: ViewController { navigationController.pushViewController(giftController, animated: true) } } - }), + }),*/ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], actionLayout: .vertical, diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index 00a0a4caa50..57777f83543 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -235,14 +235,15 @@ public func PremiumBoostScreen( title: presentationData.strings.ChannelBoost_MoreBoosts_Title, text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { + // MARK: Swiftgram + /*TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { dismissImpl?() Queue.mainQueue().after(0.4) { let controller = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost, completion: nil) pushController(controller) } - }), + }),*/ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) ], actionLayout: .vertical, diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 51a2d5e99bb..97907a7d27e 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1,3 +1,4 @@ +import SGStrings import Foundation import UIKit import Display diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index bc802fe14c3..a6296417440 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -1,3 +1,4 @@ +import SGStrings import Foundation import UIKit import Display @@ -883,6 +884,12 @@ private final class PremiumGiftScreenComponent: CombinedComponent { } func buy() { + // MARK: Swiftgram + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: self.context, title: i18n("Common.OpenTelegram", presentationData.strings.baseLanguageCode), text: i18n("Common.UseTelegramForPremium", presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) + /* + guard let inAppPurchaseManager = self.context.inAppPurchaseManager, !self.inProgress else { return } @@ -980,6 +987,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { } } }) + */ } func updateIsFocused(_ isFocused: Bool) { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6240ee08ca1..c52715c6969 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1,3 +1,4 @@ +import SGStrings import Foundation import UIKit import Display @@ -2101,7 +2102,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let isPremium = state?.isPremium == true var dismissImpl: (() -> Void)? - let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : i18n("Common.OpenTelegram", strings.baseLanguageCode), isPremium: isPremium, forceDark: forceDark) controller.action = { [weak state] in dismissImpl?() if state?.isPremium == false { @@ -3008,7 +3009,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { guard !self.inProgress else { return } + + // MARK: Swiftgram + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: self.context, title: i18n("Common.OpenTelegram", presentationData.strings.baseLanguageCode), text: i18n("Common.UseTelegramForPremium", presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + self.present(alertController) + /* if case let .gift(_, _, _, giftCode) = self.source, let giftCode, giftCode.usedDate == nil { self.inProgress = true self.updateInProgress(true) @@ -3163,7 +3170,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { strongSelf.updated(transition: .immediate) } } - }) + })*/ } func updateIsFocused(_ isFocused: Bool) { @@ -3568,9 +3575,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if isUnusedGift { buttonTitle = environment.strings.Premium_Gift_ApplyLink } else if state.isPremium == true && state.canUpgrade { - buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string + buttonTitle = i18n("Common.OpenTelegram", environment.strings.baseLanguageCode) } else { - buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string + buttonTitle = i18n("Common.OpenTelegram", environment.strings.baseLanguageCode) } let sideInset: CGFloat = 16.0 diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 1aefe3e2255..9e08a1250e2 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -190,7 +190,8 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { } }, tapAction: { _, _ in - giftPremium() + // MARK: Swiftgram + if ({ return false }()) { giftPremium() } } ), environment: {}, diff --git a/submodules/RMIntro/Sources/core/animations.c b/submodules/RMIntro/Sources/core/animations.c index 6dc53f1cccb..f126c8a7698 100644 --- a/submodules/RMIntro/Sources/core/animations.c +++ b/submodules/RMIntro/Sources/core/animations.c @@ -421,11 +421,11 @@ void on_surface_created() { mask1 = create_rounded_rectangle(CSizeMake(60, 60), 0, 16, black_color); - + // MARK: Swiftgram // Telegram - telegram_sphere = create_textured_rectangle(CSizeMake(148, 148), telegram_sphere_texture); - telegram_plane = create_textured_rectangle(CSizeMake(82, 74), telegram_plane_texture); - telegram_plane.params.anchor=xyzMake(6, -5, 0); + telegram_sphere = create_textured_rectangle(CSizeMake(150, 150), telegram_sphere_texture); + telegram_plane = create_textured_rectangle(CSizeMake(71, 103), telegram_plane_texture); + telegram_plane.params.anchor=xyzMake(0, 0, 0); diff --git a/submodules/RMIntro/Sources/platform/ios/Resources/telegram_plane1@2x.png b/submodules/RMIntro/Sources/platform/ios/Resources/telegram_plane1@2x.png index 7a5a342bc9c..7260909f913 100644 Binary files a/submodules/RMIntro/Sources/platform/ios/Resources/telegram_plane1@2x.png and b/submodules/RMIntro/Sources/platform/ios/Resources/telegram_plane1@2x.png differ diff --git a/submodules/RMIntro/Sources/platform/ios/Resources/telegram_sphere@2x.png b/submodules/RMIntro/Sources/platform/ios/Resources/telegram_sphere@2x.png index 5048850c0ba..5bb5b80fc8f 100644 Binary files a/submodules/RMIntro/Sources/platform/ios/Resources/telegram_sphere@2x.png and b/submodules/RMIntro/Sources/platform/ios/Resources/telegram_sphere@2x.png differ diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 675ccef464b..e753e9b512e 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -244,6 +244,18 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal, _ s26: Signal, _ s27: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26), signalOfAny(s27)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26, values[26] as! T27) + }, initialValues: [:], queue: queue) +} + +public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal, _ s26: Signal, _ s27: Signal, _ s28: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, T27, T28), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26), signalOfAny(s27), signalOfAny(s28)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26, values[26] as! T27, values[27] as! T28) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 347920fa643..4d1e5af9aea 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -1,5 +1,11 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//submodules/BuildConfig:BuildConfig", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGStrings:SGStrings" +] + swift_library( name = "SettingsUI", module_name = "SettingsUI", @@ -9,7 +15,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 2caf610c6a5..76e359c98cf 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -966,7 +966,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da } else if webBrowserSettings.defaultWebBrowser == "inApp" { defaultWebBrowser = presentationData.strings.WebBrowser_InAppSafari } else { - defaultWebBrowser = presentationData.strings.WebBrowser_Telegram + defaultWebBrowser = presentationData.strings.WebBrowser_Telegram.replacingOccurrences(of: "Telegram", with: "Swiftgram") } let previousSensitiveContent = sensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled) diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 48ea6da9dbd..bc5b7a96fe0 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -1,3 +1,7 @@ +// MARK: Swiftgram +import SGSimpleSettings +import SGStrings + import Foundation import UIKit import Display @@ -13,6 +17,7 @@ import UrlEscaping import ShareController private final class ProxySettingsControllerArguments { + let toggleLocalDNS: (Bool) -> Void let toggleEnabled: (Bool) -> Void let addNewServer: () -> Void let activateServer: (ProxyServerSettings) -> Void @@ -22,7 +27,8 @@ private final class ProxySettingsControllerArguments { let toggleUseForCalls: (Bool) -> Void let shareProxyList: () -> Void - init(toggleEnabled: @escaping (Bool) -> Void, addNewServer: @escaping () -> Void, activateServer: @escaping (ProxyServerSettings) -> Void, editServer: @escaping (ProxyServerSettings) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, toggleUseForCalls: @escaping (Bool) -> Void, shareProxyList: @escaping () -> Void) { + init(toggleLocalDNS: @escaping (Bool) -> Void, toggleEnabled: @escaping (Bool) -> Void, addNewServer: @escaping () -> Void, activateServer: @escaping (ProxyServerSettings) -> Void, editServer: @escaping (ProxyServerSettings) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, toggleUseForCalls: @escaping (Bool) -> Void, shareProxyList: @escaping () -> Void) { + self.toggleLocalDNS = toggleLocalDNS self.toggleEnabled = toggleEnabled self.addNewServer = addNewServer self.activateServer = activateServer @@ -60,6 +66,8 @@ private enum ProxySettingsControllerEntryId: Equatable, Hashable { private enum ProxySettingsControllerEntry: ItemListNodeEntry { case enabled(PresentationTheme, String, Bool, Bool) + case localDNSToggle(PresentationTheme, String, Bool) + case localDNSNotice(PresentationTheme, String) case serversHeader(PresentationTheme, String) case addServer(PresentationTheme, String, Bool) case server(Int, PresentationTheme, PresentationStrings, ProxyServerSettings, Bool, DisplayProxyServerStatus, ProxySettingsServerItemEditing, Bool) @@ -69,6 +77,8 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { + case .localDNSToggle, .localDNSNotice: + return ProxySettingsControllerSection.enabled.rawValue case .enabled: return ProxySettingsControllerSection.enabled.rawValue case .serversHeader, .addServer, .server: @@ -83,6 +93,10 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { var stableId: ProxySettingsControllerEntryId { switch self { case .enabled: + return .index(-2) + case .localDNSToggle: + return .index(-1) + case .localDNSNotice: return .index(0) case .serversHeader: return .index(1) @@ -107,6 +121,18 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .localDNSToggle(lhsTheme, lhsText, lhsValue): + if case let .localDNSToggle(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .localDNSNotice(lhsTheme, lhsText): + if case let .localDNSNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .serversHeader(lhsTheme, lhsText): if case let .serversHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -155,23 +181,37 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { default: return true } + case .localDNSToggle: + switch rhs { + case .enabled, .localDNSToggle: + return false + default: + return true + } + case .localDNSNotice: + switch rhs { + case .enabled, .localDNSToggle, .localDNSNotice: + return false + default: + return true + } case .serversHeader: switch rhs { - case .enabled, .serversHeader: + case .enabled, .localDNSToggle, .localDNSNotice, .serversHeader: return false default: return true } case .addServer: switch rhs { - case .enabled, .serversHeader, .addServer: + case .enabled, .localDNSToggle, .localDNSNotice, .serversHeader, .addServer: return false default: return true } case let .server(lhsIndex, _, _, _, _, _, _, _): switch rhs { - case .enabled, .serversHeader, .addServer: + case .enabled, .localDNSToggle, .localDNSNotice, .serversHeader, .addServer: return false case let .server(rhsIndex, _, _, _, _, _, _, _): return lhsIndex < rhsIndex @@ -180,14 +220,14 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } case .shareProxyList: switch rhs { - case .enabled, .serversHeader, .addServer, .server, .shareProxyList: + case .enabled, .localDNSToggle, .localDNSNotice, .serversHeader, .addServer, .server, .shareProxyList: return false default: return true } case .useForCalls: switch rhs { - case .enabled, .serversHeader, .addServer, .server, .shareProxyList, .useForCalls: + case .enabled, .localDNSToggle, .localDNSNotice, .serversHeader, .addServer, .server, .shareProxyList, .useForCalls: return false default: return true @@ -208,6 +248,12 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { arguments.toggleEnabled(value) } }) + case let .localDNSToggle(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleLocalDNS(value) + }) + case let .localDNSNotice(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .serversHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .addServer(_, text, _): @@ -242,6 +288,9 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P var entries: [ProxySettingsControllerEntry] = [] entries.append(.enabled(theme, strings.ChatSettings_ConnectionType_UseProxy, proxySettings.enabled, proxySettings.servers.isEmpty)) + // MARK: Swiftgram + entries.append(.localDNSToggle(theme, i18n("ProxySettings.UseSystemDNS", strings.baseLanguageCode), SGSimpleSettings.shared.localDNSForProxyHost)) + entries.append(.localDNSNotice(theme, i18n("ProxySettings.UseSystemDNS.Notice", strings.baseLanguageCode))) entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies)) entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing)) var index = 0 @@ -315,6 +364,7 @@ public func proxySettingsController(context: AccountContext, mode: ProxySettings public func proxySettingsController(accountManager: AccountManager, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var dismissImpl: (() -> Void)? let stateValue = Atomic(value: ProxySettingsControllerState()) let statePromise = ValuePromise(stateValue.with { $0 }) @@ -334,7 +384,25 @@ public func proxySettingsController(accountManager: AccountManager Void)? - let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in + let arguments = ProxySettingsControllerArguments(toggleLocalDNS: { value in + SGSimpleSettings.shared.localDNSForProxyHost = value + guard let context = context else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode)), + ActionSheetButtonItem(title: i18n("Common.RestartNow", presentationData.strings.baseLanguageCode), color: .destructive, font: .default, action: { + exit(0) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, toggleEnabled: { value in let _ = updateProxySettingsInteractively(accountManager: accountManager, { current in var current = current current.enabled = value @@ -530,5 +598,9 @@ public func proxySettingsController(accountManager: AccountManager take(1) |> deliverOnMainQueue ).start(next: { accountAndPeer, accountsAndPeers in - var maximumAvailableAccounts: Int = 3 + var maximumAvailableAccounts: Int = maximumSwiftgramNumberOfAccounts if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } var count: Int = 1 for (accountContext, peer, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { if peer.isPremium { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } count += 1 } @@ -226,8 +227,18 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo } pushControllerImpl?(controller) } else { - context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) - + if count + 1 > maximumSafeNumberOfAccounts { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: context, title: presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: i18n("Auth.AccountBackupReminder", presentationData.strings.baseLanguageCode), actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + }) + ], dismissOnOutsideTap: false) + presentControllerImpl?(alertController, nil) + } else { + context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + } + dismissImpl?() } }) diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 78a228ed3c6..b56bb2b43db 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -1,3 +1,4 @@ +import SGStrings import Foundation import UIKit import Display @@ -447,6 +448,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { return } + // MARK: Swiftgram let isPremium = peer?.isPremium ?? false var entries: [LanguageListEntry] = [] @@ -461,7 +463,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { var ignoredLanguages: [String] = [] if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { showTranslate = translationSettings.showTranslate - translateChats = isPremium ? translationSettings.translateChats : false + translateChats = translationSettings.translateChats if let languages = translationSettings.ignoredLanguages { ignoredLanguages = languages } else { @@ -483,7 +485,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } } } else { - translateChats = isPremium + translateChats = isPremium || true if let activeLanguage = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguage) { ignoredLanguages = [activeLanguage] } @@ -502,7 +504,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate)) - entries.append(.translateEntire(text: presentationData.strings.Localization_TranslateEntireChat, value: translateChats, locked: !isPremium)) + entries.append(.translateEntire(text: presentationData.strings.Localization_TranslateEntireChat, value: translateChats, locked: !(isPremium || true))) var value = "" if ignoredLanguages.count > 1 { @@ -552,6 +554,17 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } else { entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue)) } + + // MARK: Swiftrgam + for info in SGLocalizations { + if existingIds.contains(info.languageCode) { + continue + } + existingIds.insert(info.languageCode) + entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false)) + } + // + for info in localizationListState.availableOfficialLocalizations { if existingIds.contains(info.languageCode) { continue @@ -727,6 +740,13 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self?.applyingCode.set(.single(nil)) self?.context.engine.messages.refreshAttachMenuBots() + + // MARK: Swiftgram + // TODO(swiftgram): consider moving to downloadAndApplyLocalization for an app-wide strings update + if let baseLanguageCode = info.baseLanguageCode { + SGLocalizationManager.shared.downloadLocale(baseLanguageCode) + } + })) } if info.isOfficial { diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift index 1736b737a06..d5eb6ddcd8d 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -165,10 +165,13 @@ public func translationSettingsController(context: AccountContext) -> ViewContro } } - for code in supportedTranslationLanguages { + for code in supportedTranslationLanguages + ["zh-hans", "zh-hant"] { if !addedLanguages.contains(code), let title = enLocale.localizedString(forLanguageCode: code) { let languageLocale = Locale(identifier: code) - let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + var subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + if code == "zh-hans" || code == "zh-hant" { + subtitle += " \(code)" + } let value = (code, title.capitalized, subtitle.capitalized) if code == interfaceLanguageCode { languages.insert(value, at: 0) diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index abd5ee4b84a..ce1f4345f54 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -1,3 +1,4 @@ +import SGStrings import Foundation import UIKit import Display @@ -139,15 +140,15 @@ public func logoutOptionsController(context: AccountContext, navigationControlle |> take(1) |> deliverOnMainQueue ).start(next: { accountAndPeer, accountsAndPeers in - var maximumAvailableAccounts: Int = 3 + var maximumAvailableAccounts: Int = maximumSwiftgramNumberOfAccounts if accountAndPeer?.1.isPremium == true && !context.account.testingEnvironment { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } var count: Int = 1 for (accountContext, peer, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { if peer.isPremium { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } count += 1 } @@ -165,7 +166,17 @@ public func logoutOptionsController(context: AccountContext, navigationControlle } pushControllerImpl?(controller) } else { - context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + if count + 1 > maximumSafeNumberOfAccounts { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: context, title: presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: i18n("Auth.AccountBackupReminder", presentationData.strings.baseLanguageCode), actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + }) + ], dismissOnOutsideTap: false) + presentControllerImpl?(alertController, nil) + } else { + context.sharedContext.beginNewAuth(testingEnvironment: context.account.testingEnvironment) + } dismissImpl?() } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index 784a9b396da..6f47806d8c1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -1,3 +1,6 @@ +// MARK: Swiftgram +import SGStrings + import Foundation import UIKit import Display @@ -163,7 +166,10 @@ private struct PasscodeOptionsData: Equatable { private func autolockStringForTimeout(strings: PresentationStrings, timeout: Int32?) -> String { if let timeout = timeout { - if timeout == 10 { + // MARK: Swiftgram + if timeout == 5 { + return i18n("PasscodeSettings.AutoLock.InFiveSeconds", strings.baseLanguageCode) + } else if timeout == 10 { return "If away for 10 seconds" } else if timeout == 1 * 60 { return strings.PasscodeSettings_AutoLock_IfAwayFor_1minute @@ -321,7 +327,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { }).start() }) } - var values: [Int32] = [0, 1 * 60, 5 * 60, 1 * 60 * 60, 5 * 60 * 60] + var values: [Int32] = [0, 5, 1 * 60, 5 * 60, 1 * 60 * 60, 5 * 60 * 60] #if DEBUG values.append(10) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 1bc481e1dd0..c36469ec827 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -801,6 +801,10 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { return false } + // MARK: Swiftgram + if appConfiguration.sgWebSettings.global.qrLogin { + return true + } guard let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR else { return false } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index e22d5e2f17c..8bb9bb0c459 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -1,3 +1,4 @@ +import BuildConfig import Foundation import UIKit import Display @@ -278,6 +279,9 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe var hasSecretChats = false var hasIncomingCalls = false + let baseAppBundleId = Bundle.main.bundleIdentifier! + let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId) + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let title: String let subtitle: String @@ -286,6 +290,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe let deviceTitle: String let location: String let ip: String + var apiId: String = "" switch subject { case let .session(session): self.terminateButton.title = self.presentationData.strings.AuthSessions_View_TerminateSession @@ -305,8 +310,23 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe if !session.deviceModel.isEmpty { deviceString = session.deviceModel } +// if !session.platform.isEmpty { +// if !deviceString.isEmpty { +// deviceString += ", " +// } +// deviceString += session.platform +// } +// if !session.systemVersion.isEmpty { +// if !deviceString.isEmpty { +// deviceString += ", " +// } +// deviceString += session.systemVersion +// } + if buildConfig.apiId != session.apiId { + apiId = "\napi_id: \(session.apiId)" + } title = deviceString - device = "\(session.appName) \(appVersion)" + device = "\(session.appName) \(appVersion)\(apiId)" location = session.country ip = session.ip @@ -391,6 +411,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe self.deviceTitleNode.attributedText = NSAttributedString(string: deviceTitle, font: Font.regular(17.0), textColor: textColor) self.deviceValueNode.attributedText = NSAttributedString(string: device, font: Font.regular(17.0), textColor: secondaryTextColor) + self.deviceValueNode.maximumNumberOfLines = 2 self.deviceValueNode.accessibilityLabel = deviceTitle self.deviceValueNode.accessibilityValue = device self.deviceValueNode.isAccessibilityElement = true diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 946e81f609f..9a94ae21389 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -357,6 +357,43 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { var name = "Icon" var bordered = true switch icon.name { + case "SGDefault": + name = item.strings.Appearance_AppIconDefault + bordered = false + case "SGBlack": + name = "Black" + bordered = false + case "SGLegacy": + name = "Legacy" + bordered = false + case "SGInverted": + name = "Inverted" + case "SGWhite": + name = "White" + case "SGNight": + name = "Night" + bordered = false + case "SGSky": + name = "Sky" + bordered = false + case "SGTitanium": + name = "Titanium" + bordered = false + case "SGNeon": + name = "Neon" + bordered = false + case "SGNeonBlue": + name = "Neon Blue" + bordered = false + case "SGGlass": + name = "Glass" + bordered = false + case "SGSparkling": + name = "Sparkling" + bordered = false + case "SGBeta": + name = "β Beta" + bordered = false case "BlueIcon": name = item.strings.Appearance_AppIconDefault case "BlackIcon": diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 439be0fa1f8..cf3eecc6501 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "ShareController", module_name = "ShareController", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Postbox:Postbox", diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 09447ffa43e..72e2cfac0cf 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1,4 +1,5 @@ import Foundation +import SGSimpleSettings import UIKit import Display import AsyncDisplayKit @@ -460,7 +461,14 @@ public final class ShareController: ViewController { public var parentNavigationController: NavigationController? - public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { + public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, immediateExternalShareOverridingSGBehaviour: Bool? = nil, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { + var immediateExternalShare = immediateExternalShare + if SGSimpleSettings.shared.forceSystemSharing { + immediateExternalShare = true + } + if let immediateExternalShareOverridingSGBehaviour = immediateExternalShareOverridingSGBehaviour { + immediateExternalShare = immediateExternalShareOverridingSGBehaviour + } self.init( environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), currentContext: ShareControllerAppAccountContext(context: context), @@ -1051,7 +1059,7 @@ public final class ShareController: ViewController { var restrictedText: String? for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute { - restrictedText = attribute.platformText(platform: "ios", contentSettings: strongSelf.currentContext.contentSettings) ?? "" + restrictedText = attribute.platformText(platform: "ios", contentSettings: strongSelf.currentContext.contentSettings, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 1d4d9439c01..b508f3e945e 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -163,7 +164,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.peersValue.set(.single(peers)) - let canShareStory = controllerInteraction.shareStory != nil + let canShareStory = controllerInteraction.shareStory != nil && SGSimpleSettings.shared.showRepostToStory let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get(), self.tick.get(), self.themePromise.get()) |> map { [weak controllerInteraction] initialPeers, foundPeers, _, theme -> [SharePeerEntry] in diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 84c74e819c6..7bbfc97b014 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -107,7 +107,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe cropRect = CGRect(x: (size.width - shortestSide) / 2.0, y: (size.height - shortestSide) / 2.0, width: shortestSide, height: shortestSide) } - adjustments = TGVideoEditAdjustments(originalSize: size, cropRect: cropRect, cropOrientation: .up, cropRotation: 0.0, cropLockedAspectRatio: 1.0, cropMirrored: false, trimStartValue: 0.0, trimEndValue: 0.0, toolValues: nil, paintingData: nil, sendAsGif: false, preset: TGMediaVideoConversionPresetVideoMessage) + adjustments = TGVideoEditAdjustments(originalSize: size, cropRect: cropRect, cropOrientation: .up, cropRotation: 0.0, cropLockedAspectRatio: 1.0, cropMirrored: false, trimStartValue: 0.0, trimEndValue: 0.0, toolValues: nil, paintingData: nil, sendAsGif: false, sendAsTelescope: false, preset: TGMediaVideoConversionPresetVideoMessage) } } var finalDuration: Double = CMTimeGetSeconds(asset.duration) diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index 5b37a045b5b..4669c3bd3f7 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -437,12 +437,12 @@ public final class ShimmerEffectNode: ASDisplayNode { self.view.mask = self.foregroundNode.view } } else { - if self.view.mask != nil { - self.view.mask = nil + //if self.view.mask != nil { + // self.view.mask = nil if self.foregroundNode.supernode == nil { self.addSubnode(self.foregroundNode) } - } + //} } self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TabBarUI/BUILD b/submodules/TabBarUI/BUILD index 1abbce21935..6143529bfa7 100644 --- a/submodules/TabBarUI/BUILD +++ b/submodules/TabBarUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "TabBarUI", module_name = "TabBarUI", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index 04b67e4e6f1..8e0dd1ad348 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -28,10 +29,11 @@ final class TabBarControllerNode: ASDisplayNode { } } - init(theme: TabBarControllerTheme, navigationBarPresentationData: NavigationBarPresentationData, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { + init(showTabNames: Bool, theme: TabBarControllerTheme, navigationBarPresentationData: NavigationBarPresentationData, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { self.theme = theme self.navigationBarPresentationData = navigationBarPresentationData - self.tabBarNode = TabBarNode(theme: theme, itemSelected: itemSelected, contextAction: contextAction, swipeAction: swipeAction) + self.tabBarNode = TabBarNode(showTabNames: showTabNames, theme: theme, itemSelected: itemSelected, contextAction: contextAction, swipeAction: swipeAction) + self.tabBarNode.isHidden = SGSimpleSettings.shared.hideTabBar self.disabledOverlayNode = ASDisplayNode() self.disabledOverlayNode.backgroundColor = theme.backgroundColor.withAlphaComponent(0.5) self.disabledOverlayNode.alpha = 0.0 @@ -76,7 +78,7 @@ final class TabBarControllerNode: ASDisplayNode { transition.updateAlpha(node: self.disabledOverlayNode, alpha: value ? 0.0 : 1.0) } - var tabBarHidden = false + var tabBarHidden = SGSimpleSettings.shared.hideTabBar func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) { var tabBarHeight: CGFloat diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 1579700413f..83f1f504714 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -128,11 +129,13 @@ open class TabBarControllerImpl: ViewController, TabBarController { private let pendingControllerDisposable = MetaDisposable() private var navigationBarPresentationData: NavigationBarPresentationData + private var showTabNames: Bool private var theme: TabBarControllerTheme public var cameraItemAndAction: (item: UITabBarItem, action: () -> Void)? - public init(navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { + public init(showTabNames: Bool, navigationBarPresentationData: NavigationBarPresentationData, theme: TabBarControllerTheme) { + self.showTabNames = showTabNames self.navigationBarPresentationData = navigationBarPresentationData self.theme = theme @@ -211,6 +214,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { } public func updateIsTabBarHidden(_ value: Bool, transition: ContainedViewLayoutTransition) { + self.tabBarControllerNode.tabBarNode.isHidden = value self.tabBarControllerNode.tabBarHidden = value if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .slide)) @@ -218,7 +222,8 @@ open class TabBarControllerImpl: ViewController, TabBarController { } override open func loadDisplayNode() { - self.displayNode = TabBarControllerNode(theme: self.theme, navigationBarPresentationData: self.navigationBarPresentationData, itemSelected: { [weak self] index, longTap, itemNodes in + // MARK: Swiftgram + self.displayNode = TabBarControllerNode(showTabNames: self.showTabNames, theme: self.theme, navigationBarPresentationData: self.navigationBarPresentationData, itemSelected: { [weak self] index, longTap, itemNodes in if let strongSelf = self { var index = index if let (cameraItem, cameraAction) = strongSelf.cameraItemAndAction { diff --git a/submodules/TabBarUI/Sources/TabBarNode.swift b/submodules/TabBarUI/Sources/TabBarNode.swift index b5d14b5460d..592e86c350b 100644 --- a/submodules/TabBarUI/Sources/TabBarNode.swift +++ b/submodules/TabBarUI/Sources/TabBarNode.swift @@ -348,6 +348,8 @@ class TabBarNode: ASDisplayNode, ASGestureRecognizerDelegate { private var horizontal: Bool = false private var centered: Bool = false + private var showTabNames: Bool + private var badgeImage: UIImage let backgroundNode: NavigationBackgroundNode @@ -356,8 +358,9 @@ class TabBarNode: ASDisplayNode, ASGestureRecognizerDelegate { private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? - init(theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void) { + init(showTabNames: Bool, theme: TabBarControllerTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void) { self.itemSelected = itemSelected + self.showTabNames = showTabNames self.contextAction = contextAction self.swipeAction = swipeAction self.theme = theme @@ -734,6 +737,12 @@ class TabBarNode: ASDisplayNode, ASGestureRecognizerDelegate { node.contextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) node.contextTextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) + // MARK: Swiftgram + if !self.showTabNames { + node.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 6.0), size: nodeFrame.size) + node.textImageNode.frame = CGRect(origin: CGPoint(), size: CGSize()) + } + let scaleFactor: CGFloat = horizontal ? 0.8 : 1.0 node.animationContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0) let animationOffset: CGPoint = self.tabBarItems[i].item.animationOffset diff --git a/submodules/TelegramAudio/BUILD b/submodules/TelegramAudio/BUILD index 319722988c1..091fa7890b5 100644 --- a/submodules/TelegramAudio/BUILD +++ b/submodules/TelegramAudio/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "TelegramAudio", module_name = "TelegramAudio", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", ], visibility = [ diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index f2739727b57..be39bd7dbcd 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import SwiftSignalKit import AVFoundation @@ -1054,6 +1055,10 @@ public final class ManagedAudioSessionImpl: NSObject, ManagedAudioSession { var alreadySet = false if self.isHeadsetPluggedInValue { if case .voiceCall = updatedType, case .custom(.builtin) = outputMode { + } else if SGSimpleSettings.shared.forceBuiltInMic { + let _ = try? AVAudioSession.sharedInstance().setPreferredInput( + routes.first { $0.portType == .builtInMic } + ) } else { loop: for route in routes { switch route.portType { diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 2efdb563310..86aff6e728c 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -145,7 +145,8 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate { } private static func providerConfiguration() -> CXProviderConfiguration { - let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram") + // MARK: Swiftgram + let providerConfiguration = CXProviderConfiguration(localizedName: "Swiftgram") providerConfiguration.supportsVideo = true providerConfiguration.maximumCallsPerCallGroup = 1 diff --git a/submodules/TelegramCore/BUILD b/submodules/TelegramCore/BUILD index 2d8a1a61bc7..c51bc797fbf 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -1,5 +1,12 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SwiftSoup:SwiftSoup", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGTranslationLangFix:SGTranslationLangFix", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme" +] + swift_library( name = "TelegramCore", module_name = "TelegramCore", @@ -9,7 +16,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/TelegramApi:TelegramApi", "//submodules/MtProtoKit:MtProtoKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index a362cd626a2..6ab82b726d9 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -213,6 +213,8 @@ private var declaredEncodables: Void = { declareEncodable(AuthSessionInfoAttribute.self, f: { AuthSessionInfoAttribute(decoder: $0) }) declareEncodable(TranslationMessageAttribute.self, f: { TranslationMessageAttribute(decoder: $0) }) declareEncodable(TranslationMessageAttribute.Additional.self, f: { TranslationMessageAttribute.Additional(decoder: $0) }) + // MARK: Swiftgram + declareEncodable(QuickTranslationMessageAttribute.self, f: { QuickTranslationMessageAttribute(decoder: $0) }) declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) }) declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) }) declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 302ec5da6b6..02671fe5fc4 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -81,6 +81,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], } } if isAnimated { + // TODO(swiftgram): refinedTag = [.photoOrVideo, .video, .gif] refinedTag = .gif } if file.isAnimatedSticker { diff --git a/submodules/TelegramCore/Sources/Network/FetchV2.swift b/submodules/TelegramCore/Sources/Network/FetchV2.swift index 6d460bf17bb..7ac45f60033 100644 --- a/submodules/TelegramCore/Sources/Network/FetchV2.swift +++ b/submodules/TelegramCore/Sources/Network/FetchV2.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import Postbox import SwiftSignalKit @@ -335,9 +336,9 @@ private final class FetchImpl { } if isStory { - self.defaultPartSize = 512 * 1024 + self.defaultPartSize = getSGDownloadPartSize(512 * 1024) } else { - self.defaultPartSize = 128 * 1024 + self.defaultPartSize = getSGDownloadPartSize(128 * 1024) } self.cdnPartSize = 128 * 1024 @@ -380,7 +381,7 @@ private final class FetchImpl { maxPartSize: 1 * 1024 * 1024, partAlignment: 4 * 1024, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6 + maxPendingParts: getSGMaxPendingParts(6) )) } guard let state = self.state else { @@ -613,7 +614,7 @@ private final class FetchImpl { maxPartSize: self.cdnPartSize * 2, partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6 + maxPendingParts: getSGMaxPendingParts(6) )) self.update() }, error: { [weak self] error in @@ -661,7 +662,7 @@ private final class FetchImpl { maxPartSize: self.defaultPartSize, partAlignment: 4 * 1024, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6 + maxPendingParts: getSGMaxPendingParts(6) )) self.update() @@ -837,7 +838,7 @@ private final class FetchImpl { maxPartSize: self.cdnPartSize * 2, partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6 + maxPendingParts: getSGMaxPendingParts(6) )) case let .cdnRefresh(cdnData, refreshToken): self.state = .reuploadingToCdn(ReuploadingToCdnState( diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 3f07e3bb5ea..d72858ed3ad 100644 --- a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift +++ b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import Postbox import TelegramApi @@ -479,7 +480,8 @@ func multipartUpload(network: Network, postbox: Postbox, source: MultipartUpload } } - let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts, increaseParallelParts: increaseParallelParts, uploadPart: { part in + // TODO(swiftgram): Change other variables for uploadSpeedBoost + let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts || SGSimpleSettings.shared.uploadSpeedBoost, increaseParallelParts: increaseParallelParts || SGSimpleSettings.shared.uploadSpeedBoost, uploadPart: { part in switch uploadInterface { case let .download(download): return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError) diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index 58214119182..71a85a65318 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -1,3 +1,6 @@ +// MARK: Swiftgram +import SGSimpleSettings + import Foundation import Postbox import TelegramApi @@ -502,8 +505,8 @@ func initializedNetwork(accountId: AccountRecordId, arguments: NetworkInitializa } let useTempAuthKeys: Bool = true - - let context = MTContext(serialization: serialization, encryptionProvider: arguments.encryptionProvider, apiEnvironment: apiEnvironment, isTestingEnvironment: testingEnvironment, useTempAuthKeys: useTempAuthKeys) + let forceLocalDNS: Bool = SGSimpleSettings.shared.localDNSForProxyHost + let context = MTContext(serialization: serialization, encryptionProvider: arguments.encryptionProvider, apiEnvironment: apiEnvironment, isTestingEnvironment: testingEnvironment, useTempAuthKeys: useTempAuthKeys, forceLocalDNS: forceLocalDNS) if let networkSettings = networkSettings { let useNetworkFramework: Bool diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index a5ac46ba0bc..9a4db97c9be 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -544,11 +544,11 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, transaction.storeMediaIfNotPresent(media: file) } - for emoji in text.emojis { - if emoji.isSingleEmoji { - if !emojiItems.contains(where: { $0.content == .text(emoji) }) { - emojiItems.append(RecentEmojiItem(.text(emoji))) - } + // MARK: Swiftgram + var filteredEmojiItems = [NSRange: RecentEmojiItem]() + text.enumerateSubstrings(in: text.startIndex ..< text.endIndex, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring, substring.isSingleEmoji { + filteredEmojiItems[NSRange(range, in: text)] = RecentEmojiItem(.text(substring)) } } @@ -673,10 +673,17 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, addedHashtags.append(hashtag) } } else if case let .CustomEmoji(_, fileId) = entity.type { + // MARK: Swiftgram let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) - if let file = inlineStickers[mediaId] as? TelegramMediaFile { - emojiItems.append(RecentEmojiItem(.file(file))) - } else if let file = transaction.getMedia(mediaId) as? TelegramMediaFile { + let entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + var file: TelegramMediaFile? + if let unwrappedFile = inlineStickers[mediaId] as? TelegramMediaFile { + file = unwrappedFile + } else if let unwrappedFile = transaction.getMedia(mediaId) as? TelegramMediaFile { + file = unwrappedFile + } + if let file { + filteredEmojiItems.removeValue(forKey: entityRange) emojiItems.append(RecentEmojiItem(.file(file))) } } @@ -684,6 +691,8 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, break } } + // MARK: Swiftgram + emojiItems.insert(contentsOf: filteredEmojiItems.values, at: 0) let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: mediaList, textEntities: entitiesAttribute?.entities, isPinned: false) diff --git a/submodules/TelegramCore/Sources/Settings/ContentSettings.swift b/submodules/TelegramCore/Sources/Settings/ContentSettings.swift index 204fc79900d..f556290531b 100644 --- a/submodules/TelegramCore/Sources/Settings/ContentSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/ContentSettings.swift @@ -4,14 +4,16 @@ import TelegramApi import SwiftSignalKit public struct ContentSettings: Equatable { - public static var `default` = ContentSettings(ignoreContentRestrictionReasons: [], addContentRestrictionReasons: []) + public static var `default` = ContentSettings(ignoreContentRestrictionReasons: [], addContentRestrictionReasons: [], appConfiguration: AppConfiguration.defaultValue) public var ignoreContentRestrictionReasons: Set public var addContentRestrictionReasons: [String] + public var appConfiguration: AppConfiguration - public init(ignoreContentRestrictionReasons: Set, addContentRestrictionReasons: [String]) { + public init(ignoreContentRestrictionReasons: Set, addContentRestrictionReasons: [String], appConfiguration: AppConfiguration) { self.ignoreContentRestrictionReasons = ignoreContentRestrictionReasons self.addContentRestrictionReasons = addContentRestrictionReasons + self.appConfiguration = appConfiguration } } @@ -27,7 +29,9 @@ extension ContentSettings { addContentRestrictionReasons = addContentRestrictionReasonsData } } - self.init(ignoreContentRestrictionReasons: Set(reasons), addContentRestrictionReasons: addContentRestrictionReasons) + // MARK: Swiftgram + reasons += appConfiguration.sgWebSettings.user.expandedContentReasons() + self.init(ignoreContentRestrictionReasons: Set(reasons), addContentRestrictionReasons: addContentRestrictionReasons, appConfiguration: appConfiguration) } } diff --git a/submodules/TelegramCore/Sources/State/AppConfiguration.swift b/submodules/TelegramCore/Sources/State/AppConfiguration.swift index 7b081fb90a9..4e36d2cf8f3 100644 --- a/submodules/TelegramCore/Sources/State/AppConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/AppConfiguration.swift @@ -8,7 +8,7 @@ public func currentAppConfiguration(transaction: Transaction) -> AppConfiguratio } } -func updateAppConfiguration(transaction: Transaction, _ f: (AppConfiguration) -> AppConfiguration) { +public func updateAppConfiguration(transaction: Transaction, _ f: (AppConfiguration) -> AppConfiguration) { let current = currentAppConfiguration(transaction: transaction) let updated = f(current) if updated != current { diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index ba765fa57e1..dbc14d46aa0 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -128,3 +128,55 @@ func _internal_dismissPeerSpecificServerProvidedSuggestion(account: Account, pee } } } + + +// MARK: Swiftgram +private var dismissedSGSuggestionsPromise = ValuePromise>(Set()) +private var dismissedSGSuggestions: Set = Set() { + didSet { + dismissedSGSuggestionsPromise.set(dismissedSGSuggestions) + } +} + + +public func dismissSGProvidedSuggestion(suggestionId: String) { + dismissedSGSuggestions.insert(suggestionId) +} + +public func getSGProvidedSuggestions(account: Account) -> Signal { + let key: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration])) + + return combineLatest(account.postbox.combinedView(keys: [key]), dismissedSGSuggestionsPromise.get()) + |> map { views, dismissedSuggestionsValue -> Data? in + guard let view = views.views[key] as? PreferencesView else { + return nil + } + guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { + return nil + } + guard let announcementsString = appConfiguration.sgWebSettings.global.announcementsData, + let announcementsData = announcementsString.data(using: .utf8) else { + return nil + } + + do { + if let suggestions = try JSONSerialization.jsonObject(with: announcementsData, options: []) as? [[String: Any]] { + let filteredSuggestions = suggestions.filter { suggestion in + guard let id = suggestion["id"] as? String else { + return true + } + return !dismissedSuggestionsValue.contains(id) + } + let modifiedData = try JSONSerialization.data(withJSONObject: filteredSuggestions, options: []) + return modifiedData + } else { + return nil + } + } catch { + return nil + } + } + |> distinctUntilChanged +} + + diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AppConfiguration.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AppConfiguration.swift index fa04d5db67a..32babb772db 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_AppConfiguration.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_AppConfiguration.swift @@ -1,29 +1,37 @@ import Foundation import Postbox +import SGWebSettingsScheme public struct AppConfiguration: Codable, Equatable { + // MARK: Swiftgram + public var sgWebSettings: SGWebSettings + public var data: JSON? public var hash: Int32 public static var defaultValue: AppConfiguration { - return AppConfiguration(data: nil, hash: 0) + return AppConfiguration(sgWebSettings: SGWebSettings.defaultValue, data: nil, hash: 0) } - init(data: JSON?, hash: Int32) { + init(sgWebSettings: SGWebSettings, data: JSON?, hash: Int32) { + self.sgWebSettings = sgWebSettings self.data = data self.hash = hash } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - + + self.sgWebSettings = (try container.decodeIfPresent(SGWebSettings.self, forKey: "sg")) ?? SGWebSettings.defaultValue self.data = try container.decodeIfPresent(JSON.self, forKey: "data") self.hash = (try container.decodeIfPresent(Int32.self, forKey: "storedHash")) ?? 0 } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - + + try container.encode(self.sgWebSettings, forKey: "sg") try container.encodeIfPresent(self.data, forKey: "data") try container.encode(self.hash, forKey: "storedHash") } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift index 5239cae7693..5142086c576 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift @@ -87,3 +87,47 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable { return true } } + + + + + + + +// MARK: Swiftgram +public class QuickTranslationMessageAttribute: MessageAttribute, Equatable { + public let originalText: String + public let originalEntities: [MessageTextEntity] + + public var associatedPeerIds: [PeerId] { + return [] + } + + public init( + text: String, + entities: [MessageTextEntity] + ) { + self.originalText = text + self.originalEntities = entities + } + + required public init(decoder: PostboxDecoder) { + self.originalText = decoder.decodeStringForKey("originalText", orElse: "") + self.originalEntities = decoder.decodeObjectArrayWithDecoderForKey("originalEntities") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.originalText, forKey: "originalText") + encoder.encodeObjectArray(self.originalEntities, forKey: "originalEntities") + } + + public static func ==(lhs: QuickTranslationMessageAttribute, rhs: QuickTranslationMessageAttribute) -> Bool { + if lhs.originalText != rhs.originalText { + return false + } + if lhs.originalEntities != rhs.originalEntities { + return false + } + return true + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift index 838c889580b..c72deb7f740 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift @@ -24,3 +24,16 @@ public final class SuggestedLocalizationInfo { self.availableLocalizations = availableLocalizations } } + +// MARK: Swiftgram +// All the languages are "official" to prevent their deletion +public let SGLocalizations: [LocalizationInfo] = [ + LocalizationInfo(languageCode: "zhcncc", baseLanguageCode: "zh-hans-raw", customPluralizationCode: "zh", title: "Chinese (Simplified) zhcncc", localizedTitle: "简体中文 (聪聪) - 已更完", isOfficial: true, totalStringCount: 7160, translatedStringCount: 7144, platformUrl: "https://translations.telegram.org/zhcncc/"), + LocalizationInfo(languageCode: "taiwan", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (zh-Hant-TW) @zh_Hant_TW", localizedTitle: "正體中文", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3761, platformUrl: "https://translations.telegram.org/taiwan/"), + LocalizationInfo(languageCode: "hongkong", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (Hong Kong)", localizedTitle: "中文(香港)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 6083, platformUrl: "https://translations.telegram.org/hongkong/"), + // baseLanguageCode is actually nil, since it's an "official" beta language + LocalizationInfo(languageCode: "vi-raw", baseLanguageCode: "vi-raw", customPluralizationCode: "vi", title: "Vietnamese", localizedTitle: "Tiếng Việt (beta)", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3795, platformUrl: "https://translations.telegram.org/vi/"), + LocalizationInfo(languageCode: "hi-raw", baseLanguageCode: "hi-raw", customPluralizationCode: "hi", title: "Hindi", localizedTitle: "हिन्दी (beta)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 992, platformUrl: "https://translations.telegram.org/hi/"), + // baseLanguageCode should be changed to nil? or hy? + LocalizationInfo(languageCode: "earmenian", baseLanguageCode: "earmenian", customPluralizationCode: "hy", title: "Armenian", localizedTitle: "Հայերեն", isOfficial: true, totalStringCount: 7358, translatedStringCount: 6384, platformUrl: "https://translations.telegram.org/earmenian/") +] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index baac6eb27b2..3ae792489cf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -289,7 +289,7 @@ func _internal_getSearchMessageCount(account: Account, location: SearchMessagesL } } -func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { +func _internal_searchMessages(account: Account, location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId?, limit: Int32 = 100, forceLocal: Bool = false) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { if case let .peer(peerId, fromId, tags, reactions, threadId, minDate, maxDate) = location, fromId == nil, tags == nil, peerId == account.peerId, let reactions, let reaction = reactions.first, (minDate == nil || minDate == 0), (maxDate == nil || maxDate == 0) { return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in let messages = transaction.getMessagesWithCustomTag(peerId: peerId, namespace: Namespaces.Message.Cloud, threadId: threadId, customTag: ReactionsMessageAttribute.messageTag(reaction: reaction), from: MessageIndex.upperBound(peerId: peerId, namespace: Namespaces.Message.Cloud), includeFrom: false, to: MessageIndex.lowerBound(peerId: peerId, namespace: Namespaces.Message.Cloud), limit: 500) @@ -320,14 +320,31 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation let remoteSearchResult: Signal<(Api.messages.Messages?, Api.messages.Messages?), NoError> switch location { case let .peer(peerId, fromId, tags, reactions, threadId, minDate, maxDate): - if peerId.namespace == Namespaces.Peer.SecretChat { + if peerId.namespace == Namespaces.Peer.SecretChat || forceLocal { return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in var readStates: [PeerId: CombinedPeerReadState] = [:] var threadInfo: [MessageId: MessageHistoryThreadData] = [:] if let readState = transaction.getCombinedPeerReadState(peerId) { readStates[peerId] = readState } - let result = transaction.searchMessages(peerId: peerId, query: query, tags: tags) + // MARK: Swiftgram + var result: [Message] = [] + if forceLocal { + transaction.withAllMessages(peerId: peerId, reversed: true, { message in + if result.count >= limit { + return false + } + if let tags = tags, message.tags != tags { + return true + } + if message.text.contains(query) { + result.append(message) + } + return true + }) + } else { + result = transaction.searchMessages(peerId: peerId, query: query, tags: tags) + } for message in result { for attribute in message.attributes { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 5933679e31a..80e7679c2c5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import SwiftSignalKit import Postbox @@ -74,6 +75,13 @@ public extension TelegramEngine { public func searchMessages(location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId? = nil, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit) + // TODO(swiftgram): Try to fallback on error when searching. RX is hard... + |> mapToSignal { result -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> in + if (result.0.totalCount > 0) { + return .single(result) + } + return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit, forceLocal: true) + } } public func getSearchMessageCount(location: SearchMessagesLocation, query: String) -> Signal { @@ -535,6 +543,11 @@ public extension TelegramEngine { public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang) } + + // MARK: Swiftgram + public func translateMessagesViaText(messagesDict: [EngineMessage.Id: String], toLang: String, generateEntitiesFunction: @escaping (String) -> [MessageTextEntity]) -> Signal { + return _internal_translateMessagesViaText(account: self.account, messagesDict: messagesDict, toLang: toLang, generateEntitiesFunction: generateEntitiesFunction) + } public func translateMessages(messageIds: [EngineMessage.Id], toLang: String) -> Signal { return _internal_translateMessages(account: self.account, messageIds: messageIds, toLang: toLang) @@ -1358,6 +1371,10 @@ public extension TelegramEngine { } public func markStoryAsSeen(peerId: EnginePeer.Id, id: Int32, asPinned: Bool) -> Signal { + // MARK: Swiftgram + if SGSimpleSettings.shared.isStealthModeEnabled { + return .never() + } return _internal_markStoryAsSeen(account: self.account, peerId: peerId, id: id, asPinned: asPinned) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index 6ddcd439867..62a5612bcc2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -1,3 +1,9 @@ +#if DEBUG +import SGSimpleSettings +#endif +import SGTranslationLangFix +import SwiftSoup + import Foundation import Postbox import SwiftSignalKit @@ -17,7 +23,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie var flags: Int32 = 0 flags |= (1 << 1) - return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: [.textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))], toLang: toLang)) + return network.request(Api.functions.messages.translateText(flags: flags, peer: nil, id: nil, text: [.textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))], toLang: sgTranslationLangFix(toLang))) |> mapError { error -> TranslationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded @@ -132,7 +138,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin if id.isEmpty { msgs = .single(nil) } else { - msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: toLang)) + msgs = account.network.request(Api.functions.messages.translateText(flags: flags, peer: inputPeer, id: id, text: nil, toLang: sgTranslationLangFix(toLang))) |> map(Optional.init) |> mapError { error -> TranslationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -211,6 +217,42 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } +func _internal_translateMessagesViaText(account: Account, messagesDict: [EngineMessage.Id: String], toLang: String, generateEntitiesFunction: @escaping (String) -> [MessageTextEntity]) -> Signal { + var listOfSignals: [Signal] = [] + for (messageId, text) in messagesDict { + listOfSignals.append( + // _internal_translate(network: account.network, text: text, toLang: toLang) + // |> mapToSignal { result -> Signal in + // guard let translatedText = result else { + // return .complete() + // } + gtranslate(text, toLang) + |> mapError { _ -> TranslationError in + return .generic + } + |> mapToSignal { translatedText -> Signal in +// guard case let .result(translatedText) = result else { +// return .complete() +// } + return account.postbox.transaction { transaction in + transaction.updateMessage(messageId, update: { currentMessage in + let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: translatedText, entities: generateEntitiesFunction(translatedText), toLang: toLang) + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) } + + attributes.append(updatedAttribute) + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> castError(TranslationError.self) +// |> castError(TranslateFetchError.self) + } + ) + } + return combineLatest(listOfSignals) |> ignoreValues +} + func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: EnginePeer.Id, hidden: Bool) -> Signal { return account.postbox.transaction { transaction -> Api.InputPeer? in transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData -> CachedPeerData? in @@ -261,3 +303,182 @@ func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: Eng |> ignoreValues } } + +// TODO(swiftgram): Refactor +public struct TranslateRule: Codable { + public let name: String + public let pattern: String + public let data_check: String + public let match_group: Int +} + +public func getTranslateUrl(_ message: String,_ toLang: String) -> String { + let sanitizedMessage = message.replaceCharactersFromSet(characterSet:CharacterSet.newlines, replacementString: "
") + + var queryCharSet = NSCharacterSet.urlQueryAllowed + queryCharSet.remove(charactersIn: "+&") + return "https://translate.google.com/m?hl=en&tl=\(toLang)&sl=auto&q=\(sanitizedMessage.addingPercentEncoding(withAllowedCharacters: queryCharSet) ?? "")" +} + +func prepareResultString(_ str: String) -> String { + return str.htmlDecoded.replacingOccurrences(of: "
", with: "\n").replacingOccurrences(of: "< br>", with: "\n").replacingOccurrences(of: "
", with: "\n") +} + +var regexCache: [String: NSRegularExpression] = [:] + +public func parseTranslateResponse(_ data: String) -> String { + do { + let document = try SwiftSoup.parse(data) + + if let resultContainer = try document.select("div.result-container").first() { + // new_mobile + return prepareResultString(try resultContainer.text()) + } else if let tZero = try document.select("div.t0").first() { + // old_mobile + return prepareResultString(try tZero.text()) + } + } catch Exception.Error(let type, let message) { + #if DEBUG + SGtrace("translate", what: "Translation parser failure, An error of type \(type) occurred: \(message)") + #endif + // print("Translation parser failure, An error of type \(type) occurred: \(message)") + } catch { + #if DEBUG + SGtrace("translate", what: "Translation parser failure, An error occurred: \(error)") + #endif + // print("Translation parser failure, An error occurred: \(error)") + } + return "" +} + +public func getGoogleLang(_ userLang: String) -> String { + var lang = userLang + let rawSuffix = "-raw" + if lang.hasSuffix(rawSuffix) { + lang = String(lang.dropLast(rawSuffix.count)) + } + lang = lang.lowercased() + + // Fallback To Google lang + switch (lang) { + case "zh-hans", "zh": + return "zh-CN" + case "zh-hant": + return "zh-TW" + case "he": + return "iw" + default: + break + } + + + // Fix for pt-br and other regional langs + // https://cloud.google.com/translate/docs/languages + lang = lang.components(separatedBy: "-")[0].components(separatedBy: "_")[0] + + return lang +} + + +public enum TranslateFetchError { + case network +} + + +let TranslateSessionConfiguration = URLSessionConfiguration.ephemeral + +// Create a URLSession with the ephemeral configuration +let TranslateSession = URLSession(configuration: TranslateSessionConfiguration) + +public func requestTranslateUrl(url: URL) -> Signal { + return Signal { subscriber in + let completed = Atomic(value: false) + var request = URLRequest(url: url) + request.httpMethod = "GET" + // Set headers + request.setValue("Mozilla/4.0 (compatible;MSIE 6.0;Windows NT 5.1;SV1;.NET CLR 1.1.4322;.NET CLR 2.0.50727;.NET CLR 3.0.04506.30)", forHTTPHeaderField: "User-Agent") + let downloadTask = TranslateSession.dataTask(with: request, completionHandler: { data, response, error in + let _ = completed.swap(true) + if let response = response as? HTTPURLResponse { + if response.statusCode == 200 { + if let data = data { + if let result = String(data: data, encoding: .utf8) { + subscriber.putNext(result) + subscriber.putCompletion() + } else { + subscriber.putError(.network) + } + } else { +// print("Empty data") + subscriber.putError(.network) + } + } else { +// print("Non 200 status") + subscriber.putError(.network) + } + } else { +// print("No response (??)") + subscriber.putError(.network) + } + }) + downloadTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadTask.cancel() + } + } + } +} + + +public func gtranslate(_ text: String, _ toLang: String) -> Signal { + return Signal { subscriber in + let urlString = getTranslateUrl(text, getGoogleLang(toLang)) + let url = URL(string: urlString)! + let translateSignal = requestTranslateUrl(url: url) + var translateDisposable: Disposable? = nil + + translateDisposable = translateSignal.start(next: { + translatedHtml in + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + let result = parseTranslateResponse(translatedHtml) + #if DEBUG + SGtrace("translate", what: "Translation parsed in \(CFAbsoluteTimeGetCurrent() - startTime)") + #endif + if result.isEmpty { +// print("EMPTY RESULT") + subscriber.putError(.network) // Fake + } else { + subscriber.putNext(result) + subscriber.putCompletion() + } + + }, error: { _ in + subscriber.putError(.network) + }) + + return ActionDisposable { + translateDisposable?.dispose() + } + } +} + + +extension String { + var htmlDecoded: String { + let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [ + NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html, + NSAttributedString.DocumentReadingOptionKey.characterEncoding : String.Encoding.utf8.rawValue + ] + + let decoded = try? NSAttributedString(data: Data(utf8), options: attributedOptions, documentAttributes: nil).string + return decoded ?? self + } + + func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { + return components(separatedBy: characterSet).joined(separator: replacementString) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index af88b9dffa1..22cb22893a0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import Postbox import SwiftSignalKit @@ -1032,13 +1033,18 @@ func _internal_updatedChatListFilters(postbox: Postbox, hiddenIds: Signal map { preferences, hiddenIds -> [ChatListFilter] in let filtersState = preferences.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self) ?? ChatListFiltersState.default - return filtersState.filters.filter { filter in + var filters = filtersState.filters.filter { filter in if hiddenIds.contains(filter.id) { return false } else { return true } } + // MARK: Swiftgram + if filters.count > 1 && SGSimpleSettings.shared.allChatsHidden { + filters.removeAll { $0 == .allChats } + } + return filters } |> distinctUntilChanged } @@ -1541,4 +1547,4 @@ private func synchronizeChatListFilters(transaction: Transaction, accountPeerId: ) } } -} +} \ No newline at end of file diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index be6962553a9..e71217c28b0 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -1,5 +1,6 @@ import Foundation import Postbox +import SGSimpleSettings public let anonymousSavedMessagesId: Int64 = 2666000 @@ -28,6 +29,13 @@ public extension Peer { break } + // MARK: Swiftgram + let chatId = self.id.id._internalGetInt64Value() + if contentSettings.appConfiguration.sgWebSettings.global.forceReasons.contains(chatId) { + return "Unavailable in Swiftgram due to App Store Guidelines" + } else if contentSettings.appConfiguration.sgWebSettings.global.unforceReasons.contains(chatId) { + return nil + } if let restrictionInfo = restrictionInfo { for rule in restrictionInfo.rules { if rule.reason == "sensitive" { @@ -35,7 +43,7 @@ public extension Peer { } if rule.platform == "all" || rule.platform == platform || contentSettings.addContentRestrictionReasons.contains(rule.platform) { if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { - return rule.text + return rule.text + "\n" + "\(rule.reason)-\(rule.platform)" } } } @@ -249,8 +257,11 @@ public extension Peer { return false } } - + // MARK: Swiftgram var nameColor: PeerNameColor? { + if SGSimpleSettings.shared.accountColorsSaturation == 0 { + return nil + } switch self { case let user as TelegramUser: if let nameColor = user.nameColor { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 373ef8fe9e6..abe33848fc2 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -283,6 +283,10 @@ public enum PresentationResourceKey: Int32 { case chatFreeCloseButtonIcon case chatFreeMoreButtonIcon + // MARK: Swiftgram + case chatTranslateButtonIcon + case chatUndoTranslateButtonIcon + case chatKeyboardActionButtonMessageIcon case chatKeyboardActionButtonLinkIcon case chatKeyboardActionButtonShareIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index d8406d8dce5..75a4dd5abb5 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1120,6 +1120,12 @@ public struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SideCloseIcon"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper)) }) } + // MARK: Swiftgram + public static func chatTranslateShareButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper, undoTranslate: Bool = false) -> UIImage? { + return theme.image(undoTranslate ? PresentationResourceKey.chatUndoTranslateButtonIcon.rawValue : PresentationResourceKey.chatTranslateButtonIcon.rawValue, { _ in + return generateTintedImage(image: UIImage(bundleImageName: undoTranslate ? "Media Editor/Undo" : "Chat/Context Menu/Translate"), color: bubbleVariableColor(variableColor: theme.chat.message.shareButtonForegroundColor, wallpaper: wallpaper), customSize: CGSize(width: 18.0, height: 18.0)) + }) + } public static func chatFreeMoreButtonIcon(_ theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIImage? { return theme.image(PresentationResourceKey.chatFreeMoreButtonIcon.rawValue, { _ in diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 2eef259c90e..e35b9a7c3ac 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -63,6 +63,7 @@ private func renderIcon(name: String, scaleFactor: CGFloat = 1.0, backgroundColo } public struct PresentationResourcesSettings { + public static let swiftgram = renderIcon(name: "SwiftgramSettings", scaleFactor: 30.0 / 512.0) public static let editProfile = renderIcon(name: "Settings/Menu/EditProfile") public static let proxy = renderIcon(name: "Settings/Menu/Proxy") public static let savedMessages = renderIcon(name: "Settings/Menu/SavedMessages") diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index 430fde43d63..569eff8c9c9 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -193,3 +193,32 @@ public func roundDateToDays(_ timestamp: Int32) -> Int32 { } return Int32(date.timeIntervalSince1970) } + + + + + + + + + + +// MARK: Swiftgram +public func stringForDateWithoutDay(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.timeZone = timeZone + formatter.locale = localeWithStrings(strings) + formatter.setLocalizedDateFormatFromTemplate("MMMMyyyy") + return formatter.string(from: date) +} + + +public func stringForDateWithoutDayAndMonth(date: Date, timeZone: TimeZone? = TimeZone(secondsFromGMT: 0), strings: PresentationStrings) -> String { + let formatter = DateFormatter() + formatter.timeStyle = .none + formatter.timeZone = timeZone + formatter.locale = localeWithStrings(strings) + formatter.setLocalizedDateFormatFromTemplate("yyyy") + return formatter.string(from: date) +} diff --git a/submodules/TelegramStringFormatting/Sources/Geo.swift b/submodules/TelegramStringFormatting/Sources/Geo.swift index cb065e12d72..9e4668e980f 100644 --- a/submodules/TelegramStringFormatting/Sources/Geo.swift +++ b/submodules/TelegramStringFormatting/Sources/Geo.swift @@ -49,6 +49,9 @@ public func flagEmoji(countryCode: String) -> String { if countryCode.uppercased() == "FT" { return "🏴‍☠️" } + if countryCode.uppercased() == "XX" { + return "🏳️" + } let base : UInt32 = 127397 var flagString = "" for v in countryCode.uppercased().unicodeScalars { diff --git a/submodules/TelegramStringFormatting/Sources/Locale.swift b/submodules/TelegramStringFormatting/Sources/Locale.swift index 468bd87bfc1..348568e9f8a 100644 --- a/submodules/TelegramStringFormatting/Sources/Locale.swift +++ b/submodules/TelegramStringFormatting/Sources/Locale.swift @@ -13,7 +13,16 @@ private let systemLocaleRegionSuffix: String = { public let usEnglishLocale = Locale(identifier: "en_US") public func localeWithStrings(_ strings: PresentationStrings) -> Locale { - let languageCode = strings.baseLanguageCode + var languageCode = strings.baseLanguageCode + + // MARK: - Swiftgram fix for locale bugs, like location crash + if #available(iOS 18, *) { + let rawSuffix = "-raw" + if languageCode.hasSuffix(rawSuffix) { + languageCode = String(languageCode.dropLast(rawSuffix.count)) + } + } + let code = languageCode + systemLocaleRegionSuffix return Locale(identifier: code) } diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index e289c562b6e..46ab3288bf8 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -279,7 +279,7 @@ public func messageTextWithAttributes(message: EngineMessage) -> NSAttributedStr public func messageContentKind(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> MessageContentKind { for attribute in message.attributes { if let attribute = attribute as? RestrictedContentMessageAttribute { - if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings) { + if let text = attribute.platformText(platform: "ios", contentSettings: contentSettings, chatId: message.author?.id.id._internalGetInt64Value()) { return .restricted(text) } break diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index ff1d488ded3..e0f95cb3040 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -5,6 +5,26 @@ load( "telegram_bundle_id", ) +sgdeps = [ + "//Swiftgram/SGSettingsUI:SGSettingsUI", + "//Swiftgram/SGConfig:SGConfig", + "//Swiftgram/SGAPIWebSettings:SGAPIWebSettings", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SFSafariViewControllerPlus:SFSafariViewControllerPlus", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGActionRequestHandlerSanitizer:SGActionRequestHandlerSanitizer", + "//Swiftgram/Wrap:Wrap", + "//Swiftgram/SGDeviceToken:SGDeviceToken", + "//Swiftgram/SGDebugUI:SGDebugUI" + # "//Swiftgram/SGContentAnalysis:SGContentAnalysis" +] + +sgsrcs = [ + "//Swiftgram/SGDBReset:SGDBReset", + "//Swiftgram/SGShowMessageJson:SGShowMessageJson", + "//Swiftgram/ChatControllerImplExtension:ChatControllerImplExtension" +] filegroup( name = "TelegramUIResources", @@ -44,11 +64,11 @@ swift_library( module_name = "TelegramUI", srcs = glob([ "Sources/**/*.swift", - ]), + ]) + sgsrcs, copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SSignalKit:SSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 9e19fb46b41..b5c6e1cf4f3 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1448,7 +1448,8 @@ final class AvatarEditorScreenComponent: Component { cropMirrored: false, toolValues: [:], paintingData: paintingData, - sendAsGif: true + sendAsGif: true, + sendAsTelescope: false ) let preset: TGMediaVideoConversionPreset = TGMediaVideoConversionPresetProfileHigh diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index acf4bc4dbdc..8530b1c43cf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -108,9 +108,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { public var emojiString: String? private let disposable = MetaDisposable() private let disposables = DisposableSet() + + // MARK: Swiftgram + public var sizeCoefficient: Float = 1.0 private var viaBotNode: TextNode? - private let dateAndStatusNode: ChatMessageDateAndStatusNode + public let dateAndStatusNode: ChatMessageDateAndStatusNode private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundContent: WallpaperBubbleBackgroundNode? @@ -795,7 +798,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { - var displaySize = CGSize(width: 180.0, height: 180.0) + var displaySize = CGSize(width: 180.0 * CGFloat(self.sizeCoefficient), height: 180.0 * CGFloat(self.sizeCoefficient)) let telegramFile = self.telegramFile let emojiFile = self.emojiFile let telegramDice = self.telegramDice @@ -827,7 +830,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var imageBottomPadding: CGFloat = 0.0 var imageHorizontalOffset: CGFloat = 0.0 if !(telegramFile?.videoThumbnails.isEmpty ?? true) { - displaySize = CGSize(width: 240.0, height: 240.0) + displaySize = CGSize(width: 240.0 * CGFloat(self.sizeCoefficient), height: 240.0 * CGFloat(self.sizeCoefficient)) imageVerticalInset = -20.0 imageHorizontalOffset = 12.0 } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index a0c1bb4171d..549a1045da3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -1,15 +1,25 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//submodules/TranslateUI:TranslateUI" +] + +sgsrc = [ + "//Swiftgram/SGDoubleTapMessageAction:SGDoubleTapMessageAction" +] + swift_library( name = "ChatMessageBubbleItemNode", module_name = "ChatMessageBubbleItemNode", - srcs = glob([ + srcs = sgsrc + glob([ "Sources/**/*.swift", ]), copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 59ac95207b9..c33f02cd5ce 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1,3 +1,6 @@ +import SGStrings +import SGSimpleSettings +import TranslateUI import Foundation import UIKit import AsyncDisplayKit @@ -124,7 +127,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ outer: for (message, itemAttributes) in item.content { for attribute in message.attributes { - if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil { + if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) != nil { result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false break outer @@ -280,6 +283,35 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ isMediaInverted = true } + + // MARK: Swiftgram + var message = message + if message.canRevealContent(contentSettings: item.context.currentContentSettings.with { $0 }) { + let originalTextLength = message.text.count + let noticeString = i18n("Message.HoldToShowOrReport", item.presentationData.strings.baseLanguageCode) + + message = message.withUpdatedText(message.text + "\n" + noticeString) + let noticeStringLength = noticeString.count + let startIndex = originalTextLength + 1 // +1 for the newline character + // Calculate the end index, which is the start index plus the length of noticeString + let endIndex = startIndex + noticeStringLength + + var newAttributes = message.attributes + newAttributes.append( + TextEntitiesMessageAttribute( + entities: [ + MessageTextEntity( + range: startIndex.. baseWidth { + if (needsShareButton || isAd || localNeedsQuickTranslateButton) && tmpWidth + 32.0 > baseWidth { tmpWidth = baseWidth - 32.0 } } @@ -1742,11 +1793,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } tmpWidth -= deliveryFailedInset + // MARK: Swifgram + let renderWideChannelPosts: Bool + if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts { + renderWideChannelPosts = true + + tmpWidth = baseWidth + needsShareButton = false + localNeedsQuickTranslateButton = false + } else { + renderWideChannelPosts = false + } let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item) var maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) - if needsShareButton { + if needsShareButton || localNeedsQuickTranslateButton { maximumContentWidth -= 10.0 } @@ -2218,13 +2280,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)? if let mosaicRange = mosaicRange { - let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) - let (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in + // MARK: Swiftgram + var maxDimensions = layoutConstants.image.maxDimensions + if renderWideChannelPosts { + maxDimensions.width = maximumContentWidth + } + var maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) + var (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in guard let size = item.0, size.width > 0.0, size.height > 0 else { return CGSize(width: 256.0, height: 256.0) } return size }) + // MARK: Swiftgram + if innerSize.height > maxSize.height, maxDimensions.width != layoutConstants.image.maxDimensions.width { + maxDimensions.width = max(round(maxDimensions.width * maxSize.height / innerSize.height), layoutConstants.image.maxDimensions.width) + maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right) + (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in + guard let size = item.0, size.width > 0.0, size.height > 0 else { + return CGSize(width: 256.0, height: 256.0) + } + return size + }) + } let framesAndPositions = innerFramesAndPositions.map { ($0.0.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top), $0.1) } @@ -4281,6 +4359,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI shareButtonNode.removeFromSupernode() } + // MARK: Swiftgram + // TODO(swiftgram): Move business-logic up to hierarchy + if strongSelf.needsQuickTranslateButton && incoming && !item.message.text.isEmpty && item.message.adAttribute == nil { + if strongSelf.quickTranslateButtonNode == nil { + let quickTranslateButtonNode = ChatMessageShareButton() + strongSelf.quickTranslateButtonNode = quickTranslateButtonNode + strongSelf.insertSubnode(quickTranslateButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) + quickTranslateButtonNode.pressed = { [weak strongSelf] in + strongSelf?.quickTranslateButtonPressed() + } + } + } else if let quickTranslateButtonNode = strongSelf.quickTranslateButtonNode { + strongSelf.quickTranslateButtonNode = nil + quickTranslateButtonNode.removeFromSupernode() + } + let offset: CGFloat = params.leftInset + (incoming ? 42.0 : 0.0) let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: params.width, height: layout.contentSize.height)) strongSelf.selectionNode?.frame = selectionFrame @@ -4443,6 +4537,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI animation.animator.updateAlpha(layer: shareButtonNode.layer, alpha: isCurrentlyPlayingMedia ? 0.0 : 1.0, completion: nil) } + // MARK: Swiftgram + if let quickTranslateButtonNode = strongSelf.quickTranslateButtonNode { + let currentBackgroundFrame = strongSelf.backgroundNode.frame + let buttonSize = quickTranslateButtonNode.update(hasTranslation: false /*item.message.attributes.first(where: { $0 is QuickTranslationMessageAttribute }) as? QuickTranslationMessageAttribute != nil*/, presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments) + + var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? currentBackgroundFrame.minX - buttonSize.width : currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) + + if let shareButtonOffset = shareButtonOffset { + buttonFrame.origin.x = shareButtonOffset.x + buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0) + } else if !disablesComments { + buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0) + } + + // Spacing from current shareButton + if let shareButtonNode = strongSelf.shareButtonNode { + buttonFrame.origin.y += -4.0 - shareButtonNode.frame.height + } + + animation.animator.updateFrame(layer: quickTranslateButtonNode.layer, frame: buttonFrame, completion: nil) + animation.animator.updateAlpha(layer: quickTranslateButtonNode.layer, alpha: isCurrentlyPlayingMedia ? 0.0 : 1.0, completion: nil) + + } } else { /*if let _ = strongSelf.backgroundFrameTransition { strongSelf.animateFrameTransition(1.0, backgroundFrame.size.height) @@ -4469,6 +4586,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI shareButtonNode.frame = buttonFrame shareButtonNode.alpha = isCurrentlyPlayingMedia ? 0.0 : 1.0 } + + // MARK: Swiftgram + if let quickTranslateButtonNode = strongSelf.quickTranslateButtonNode { + let buttonSize = quickTranslateButtonNode.update(hasTranslation: false /*item.message.attributes.first(where: { $0 is QuickTranslationMessageAttribute }) as? QuickTranslationMessageAttribute != nil*/, presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments) + + var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize) + if let shareButtonOffset = shareButtonOffset { + if incoming { + buttonFrame.origin.x = shareButtonOffset.x + } + buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0) + } else if !disablesComments { + buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0) + } + + // Spacing from current shareButton + if let shareButtonNode = strongSelf.shareButtonNode { + buttonFrame.origin.y += -4.0 - shareButtonNode.frame.height + } + + quickTranslateButtonNode.frame = buttonFrame + quickTranslateButtonNode.alpha = isCurrentlyPlayingMedia ? 0.0 : 1.0 + } if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview { legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame) @@ -4604,18 +4744,32 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI case let .optionalAction(f): f() case let .openContextMenu(openContextMenu): + switch (sgDoubleTapMessageAction(incoming: openContextMenu.tapMessage.effectivelyIncoming(item.context.account.peerId), message: openContextMenu.tapMessage)) { + case SGSimpleSettings.MessageDoubleTapAction.none.rawValue: + break + case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue: + item.controllerInteraction.sgStartMessageEdit(openContextMenu.tapMessage) + default: if canAddMessageReactions(message: openContextMenu.tapMessage) { item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default, false, nil) } else { item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil) } + } } } else if case .tap = gesture { item.controllerInteraction.clickThroughMessage(self.view, location) } else if case .doubleTap = gesture { + switch (sgDoubleTapMessageAction(incoming: item.message.effectivelyIncoming(item.context.account.peerId), message: item.message)) { + case SGSimpleSettings.MessageDoubleTapAction.none.rawValue: + break + case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue: + item.controllerInteraction.sgStartMessageEdit(item.message) + default: if canAddMessageReactions(message: item.message) { item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil) } + } } } default: @@ -5250,6 +5404,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event) } + // MARK: Swiftgram + if let quickTranslateButtonNode = self.quickTranslateButtonNode, quickTranslateButtonNode.frame.contains(point) { + return quickTranslateButtonNode.view.hitTest(self.view.convert(point, to: quickTranslateButtonNode.view), with: event) + } if let selectionNode = self.selectionNode { if let result = self.traceSelectionNodes(parent: self, point: point.offsetBy(dx: -42.0, dy: 0.0)) { @@ -5643,6 +5801,82 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private func updateParentMessageIsTranslating(_ isTranslating: Bool) { + for contentNode in self.contentNodes { + if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { + contentNode.updateIsTranslating(isTranslating) + } + } + } + + @objc private func quickTranslateButtonPressed() { + if let item = self.item { + let translateToLanguage = item.associatedData.translateToLanguageSG ?? item.presentationData.strings.baseLanguageCode + if let quickTranslationAttribute = item.message.attributes.first(where: { $0 is QuickTranslationMessageAttribute }) as? QuickTranslationMessageAttribute { + let _ = (item.context.account.postbox.transaction { transaction in + transaction.updateMessage(item.message.id, update: { currentMessage in + var attributes = currentMessage.attributes + + // Restore entities + attributes = attributes.filter { !($0 is TextEntitiesMessageAttribute) } + attributes.append(TextEntitiesMessageAttribute(entities: quickTranslationAttribute.originalEntities)) + + // Remove quick translation mark and Telegram's translation data to prevent bugs + attributes = attributes.filter { !($0 is QuickTranslationMessageAttribute) } + + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: quickTranslationAttribute.originalText, attributes: attributes, media: currentMessage.media)) + }) + + }).start() + } else { + Queue.mainQueue().async { + self.updateParentMessageIsTranslating(true) + } + let _ = translateMessageIds(context: item.context, messageIds: [item.message.id], toLang: translateToLanguage, viaText: !item.context.isPremium, forQuickTranslate: true).startStandalone(completed: { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + let _ = (item.context.account.postbox.transaction { transaction in + transaction.updateMessage(item.message.id, update: { currentMessage in + // Searching for succesfull translation + var translationAttribute: TranslationMessageAttribute? = nil + for attribute in currentMessage.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + translationAttribute = attribute + break + } + } + + if let translationAttribute = translationAttribute { + var attributes = currentMessage.attributes + // Replace entities + attributes = attributes.filter { !($0 is TextEntitiesMessageAttribute) } + attributes.append(TextEntitiesMessageAttribute(entities: translationAttribute.entities)) + + // Mark message as quickly translated + attributes.append(QuickTranslationMessageAttribute(text: currentMessage.text, entities: currentMessage.textEntitiesAttribute?.entities ?? [])) + + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: translationAttribute.text, attributes: attributes, media: currentMessage.media)) + } else { + return .skip + } + + }) + }).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().async { + strongSelf.updateParentMessageIsTranslating(false) + } + } + }) + } + }) + } + + + } + } + @objc private func shareButtonPressed() { if let item = self.item { if item.message.adAttribute != nil { @@ -5893,6 +6127,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI shareButtonNode.updateAbsoluteRect(shareButtonNodeFrame, within: containerSize) } + if let quickTranslateButtonNode = self.quickTranslateButtonNode { + var quickTranslateButtonNodeFrame = quickTranslateButtonNode.frame + quickTranslateButtonNodeFrame.origin.x += rect.minX + quickTranslateButtonNodeFrame.origin.y += rect.minY + + quickTranslateButtonNode.updateAbsoluteRect(quickTranslateButtonNodeFrame, within: containerSize) + } + if let actionButtonsNode = self.actionButtonsNode { var actionButtonsNodeFrame = actionButtonsNode.frame actionButtonsNodeFrame.origin.x += rect.minX diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD index 87db0a75553..2eb7c11af53 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "ChatMessageDateAndStatusNode", module_name = "ChatMessageDateAndStatusNode", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit", "//submodules/Postbox", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 7d076a23c51..19c8e859002 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -1299,5 +1300,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { } public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool { - return false + // MARK: Swiftgram + // With 10.13 it now hides reactions in favor of message effect badge + return SGSimpleSettings.shared.hideReactions } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index ada3bff052d..360aa8057a3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -345,7 +345,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { return } - if !context.isPremium, case .inProgress = self.audioTranscriptionState { + if /*!context.isPremium,*/ case .inProgress = self.audioTranscriptionState { return } @@ -353,7 +353,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message) - if transcriptionText == nil && !arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { + // MARK: Swiftgram + if transcriptionText == nil && false { if premiumConfiguration.audioTransciptionTrialCount > 0 { if !arguments.associatedData.isPremium { if self.presentAudioTranscriptionTooltip(finished: false) { @@ -412,7 +413,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { self.audioTranscriptionState = .inProgress self.requestUpdateLayout(true) - if context.sharedContext.immediateExperimentalUISettings.localTranscription { + if context.sharedContext.immediateExperimentalUISettings.localTranscription || !arguments.associatedData.isPremium { let appLocale = presentationData.strings.baseLanguageCode let signal: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: message.id)) @@ -759,7 +760,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { displayTranscribe = false } else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) - if arguments.associatedData.isPremium { + // MARK: Swiftgram + if arguments.associatedData.isPremium || true { displayTranscribe = true } else if premiumConfiguration.audioTransciptionTrialCount > 0 { if arguments.incoming { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 9bd38d928d1..f8f5de6e6bd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -204,6 +204,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } deinit { + self.transcribeDisposable?.dispose() self.fetchDisposable.dispose() self.playbackStatusDisposable.dispose() self.playerStatusDisposable.dispose() @@ -1881,6 +1882,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } } + // TODO(swiftgram): Transcribe Video Messages if shouldBeginTranscription { if self.transcribeDisposable == nil { self.audioTranscriptionState = .inProgress diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 57d0a07d667..44732c7f9c6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "ChatMessageInteractiveMediaNode", module_name = "ChatMessageInteractiveMediaNode", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit", "//submodules/Postbox", "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 707944d151b..db3df334ea5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -811,6 +812,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var isSticker = false var maxDimensions = layoutConstants.image.maxDimensions var maxHeight = layoutConstants.image.maxDimensions.height + // MARK: Swiftgram + var imageOriginalMaxDimensions: CGSize? var isStory = false let _ = isStory @@ -832,6 +835,19 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } else if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5))) + // MARK: Swiftgram + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts { + imageOriginalMaxDimensions = maxDimensions + switch sizeCalculation { + case let .constrained(constrainedSize): + maxDimensions.width = constrainedSize.width + case .unconstrained: + maxDimensions.width = unboundSize.width + } + if message.text.isEmpty { + maxDimensions.width = max(layoutConstants.image.maxDimensions.width, unboundSize.aspectFitted(CGSize(width: maxDimensions.width, height: layoutConstants.image.minDimensions.height)).width) + } + } } else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions { if let thumbnail = file.previewRepresentations.first { let dimensionsVertical = dimensions.width < dimensions.height @@ -1042,6 +1058,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } boundingSize = CGSize(width: boundingWidth, height: filledSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: maxHeight)) + if let imageOriginalMaxDimensions = imageOriginalMaxDimensions { + boundingSize.height = min(boundingSize.height, nativeSize.aspectFitted(imageOriginalMaxDimensions).height) + } boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height) boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width) switch contentMode { @@ -2436,6 +2455,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr icon = .eye } } + if displaySpoiler, let context = self.context { let extendedMediaOverlayNode: ExtendedMediaOverlayNode diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD index e704fe56750..95ffae0b7e6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/BUILD @@ -1,5 +1,10 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//submodules/TranslateUI:TranslateUI" +] + swift_library( name = "ChatMessageItemImpl", module_name = "ChatMessageItemImpl", @@ -9,7 +14,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/Postbox", "//submodules/AsyncDisplayKit", "//submodules/Display", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 415e2065ffc..9d4b501e4f6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -1,3 +1,5 @@ +import SGSimpleSettings +import TranslateUI import Foundation import UIKit import Postbox @@ -471,8 +473,36 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } + // MARK: Swiftgram + let needsQuickTranslateButton: Bool + if viewClassName == ChatMessageBubbleItemNode.self { + if self.message.attributes.first(where: { $0 is QuickTranslationMessageAttribute }) as? QuickTranslationMessageAttribute != nil { + needsQuickTranslateButton = true + } else { + let (canTranslate, _) = canTranslateText(context: self.context, text: self.message.text, showTranslate: SGSimpleSettings.shared.quickTranslateButton, showTranslateIfTopical: false, ignoredLanguages: self.associatedData.translationSettings?.ignoredLanguages) + needsQuickTranslateButton = canTranslate + } + } else { + needsQuickTranslateButton = false + } + let configure = { let node = (viewClassName as! ChatMessageItemView.Type).init(rotated: self.controllerInteraction.chatIsRotated) + // MARK: Swiftgram + if let node = node as? ChatMessageBubbleItemNode { + node.needsQuickTranslateButton = needsQuickTranslateButton + } + if let node = node as? ChatMessageStickerItemNode { + node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0 + if !SGSimpleSettings.shared.stickerTimestamp { + node.dateAndStatusNode.isHidden = true + } + } else if let node = node as? ChatMessageAnimatedStickerItemNode { + node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0 + if !SGSimpleSettings.shared.stickerTimestamp { + node.dateAndStatusNode.isHidden = true + } + } node.setupItem(self, synchronousLoad: synchronousLoads) let nodeLayout = node.asyncLayout() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 8d5e6332931..05190dd4bb7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -66,7 +66,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? RestrictedContentMessageAttribute { - rawText = attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) ?? "" + rawText = attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index fd68944f0a3..9e364a0078d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -65,6 +65,9 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: HighlightableButtonNode private let reportButton: HighlightableButtonNode private let forwardButton: HighlightableButtonNode + // MARK: Swiftgram + private let cloudButton: HighlightableButtonNode + private let forwardHideNamesButton: HighlightableButtonNode private let shareButton: HighlightableButtonNode private let tagButton: HighlightableButtonNode private let tagEditButton: HighlightableButtonNode @@ -106,7 +109,16 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0))) self.forwardButton.isAccessibilityElement = true self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward + + // MARK: Swiftgram + self.cloudButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0))) + self.cloudButton.isAccessibilityElement = true + self.cloudButton.accessibilityLabel = "Save To Cloud" + self.forwardHideNamesButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0))) + self.forwardHideNamesButton.isAccessibilityElement = true + self.forwardHideNamesButton.accessibilityLabel = "Hide Sender Name" + self.shareButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0))) self.shareButton.isAccessibilityElement = true self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare @@ -150,6 +162,19 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.isImplicitlyDisabled = true self.shareButton.isImplicitlyDisabled = true + // MARK: Swiftgram + self.cloudButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "SaveToCloud"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.cloudButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "SaveToCloud"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.addSubnode(self.cloudButton) + self.cloudButton.isImplicitlyDisabled = true + self.cloudButton.addTarget(self, action: #selector(self.cloudButtonPressed), forControlEvents: .touchUpInside) + + self.forwardHideNamesButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: theme.chat.inputPanel.panelControlAccentColor, customSize: CGSize(width: 28.0, height: 28.0)), for: [.normal]) + self.forwardHideNamesButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: theme.chat.inputPanel.panelControlDisabledColor, customSize: CGSize(width: 28.0, height: 28.0)), for: [.disabled]) + self.addSubnode(self.forwardHideNamesButton) + self.forwardHideNamesButton.isImplicitlyDisabled = true + self.forwardHideNamesButton.addTarget(self, action: #selector(self.forwardHideNamesButtonPressed), forControlEvents: .touchUpInside) + self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside) self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside) self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside) @@ -164,6 +189,9 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private func updateActions() { self.forwardButton.isEnabled = self.selectedMessages.count != 0 + // MARK: Swiftgram + self.cloudButton.isEnabled = self.forwardButton.isEnabled + self.forwardHideNamesButton.isEnabled = self.forwardButton.isEnabled if self.selectedMessages.isEmpty { self.actions = nil @@ -194,6 +222,11 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + // MARK: Swiftgram + self.cloudButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "SaveToCloud"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.cloudButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "SaveToCloud"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.forwardHideNamesButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: theme.chat.inputPanel.panelControlAccentColor, customSize: CGSize(width: 28.0, height: 28.0)), for: [.normal]) + self.forwardHideNamesButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: theme.chat.inputPanel.panelControlDisabledColor, customSize: CGSize(width: 28.0, height: 28.0)), for: [.disabled]) self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) self.tagButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/WebpageIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) @@ -218,7 +251,30 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if let actions = self.actions, actions.isCopyProtected { self.interfaceInteraction?.displayCopyProtectionTip(self.forwardButton, false) } else if !self.forwardButton.isImplicitlyDisabled { - self.interfaceInteraction?.forwardSelectedMessages() + self.interfaceInteraction?.forwardSelectedMessages(nil) + } + } + + // MARK: Swiftgram + @objc private func cloudButtonPressed() { + if let _ = self.presentationInterfaceState?.renderedPeer?.peer as? TelegramSecretChat { + return + } + if let actions = self.actions, actions.isCopyProtected { + self.interfaceInteraction?.displayCopyProtectionTip(self.cloudButton, false) + } else { + self.interfaceInteraction?.forwardSelectedMessages("toCloud") + } + } + + @objc private func forwardHideNamesButtonPressed() { + if let _ = self.presentationInterfaceState?.renderedPeer?.peer as? TelegramSecretChat { + return + } + if let actions = self.actions, actions.isCopyProtected { + self.interfaceInteraction?.displayCopyProtectionTip(self.forwardHideNamesButton, false) + } else { + self.interfaceInteraction?.forwardSelectedMessages("hideNames") } } @@ -365,6 +421,9 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.deleteButton.isEnabled = false self.reportButton.isEnabled = false self.forwardButton.isImplicitlyDisabled = !actions.options.contains(.forward) + // MARK: Swiftgram + self.cloudButton.isImplicitlyDisabled = self.forwardButton.isImplicitlyDisabled + self.forwardHideNamesButton.isImplicitlyDisabled = self.forwardButton.isImplicitlyDisabled if self.peerMedia { self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty @@ -404,6 +463,9 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.tagEditButton.isHidden = true self.tagButton.isHidden = true self.tagEditButton.isHidden = true + // MARK: Swiftgram + self.cloudButton.isImplicitlyDisabled = self.forwardButton.isImplicitlyDisabled + self.forwardHideNamesButton.isImplicitlyDisabled = self.forwardButton.isImplicitlyDisabled } if self.reportButton.isHidden || (self.peerMedia && self.deleteButton.isHidden && self.reportButton.isHidden) { @@ -426,7 +488,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { tagButton = self.tagEditButton } - let buttons: [HighlightableButtonNode] + var buttons: [HighlightableButtonNode] if self.reportButton.isHidden { if let tagButton { buttons = [ @@ -478,6 +540,18 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } } + // MARK: Swiftgram + reportButton.isHidden = true + buttons = [ + self.deleteButton, + self.reportButton, + self.tagButton, + self.shareButton, + self.cloudButton, + self.forwardHideNamesButton, + self.forwardButton + ].filter { !$0.isHidden } + let buttonSize = CGSize(width: 57.0, height: panelHeight) let availableWidth = width - leftInset - rightInset diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift index 5c6d37291b8..30dc1864bc2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift @@ -77,7 +77,7 @@ public class ChatMessageShareButton: ASDisplayNode { self.morePressed?() } - public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize { + public func update(hasTranslation: Bool? = nil, presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize { var isReplies = false var replyCount = 0 if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { @@ -121,6 +121,8 @@ public class ChatMessageShareButton: ASDisplayNode { } else if case let .customChatContents(contents) = subject, case .hashTagSearch = contents.kind { updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) + } else if let hasTranslation = hasTranslation { + updatedIconImage = PresentationResourcesChat.chatTranslateShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, undoTranslate: hasTranslation) } else if case .pinnedMessages = subject { updatedIconImage = PresentationResourcesChat.chatFreeNavigateButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) updatedIconOffset = CGPoint(x: UIScreenPixel, y: 1.0) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c63976ce117..2121fbd0417 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -51,8 +51,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { public var telegramFile: TelegramMediaFile? private let fetchDisposable = MetaDisposable() + // MARK: Swiftgram + public var sizeCoefficient: Float = 1.0 + private var viaBotNode: TextNode? - private let dateAndStatusNode: ChatMessageDateAndStatusNode + public let dateAndStatusNode: ChatMessageDateAndStatusNode private var threadInfoNode: ChatMessageThreadInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundContent: WallpaperBubbleBackgroundNode? @@ -416,7 +419,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { - let displaySize = CGSize(width: 184.0, height: 184.0) + let displaySize = CGSize(width: 184.0 * CGFloat(self.sizeCoefficient), height: 184.0 * CGFloat(self.sizeCoefficient)) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants let imageLayout = self.imageNode.asyncLayout() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 2c88943fafa..42928c3db5b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1036,7 +1036,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return super.hitTest(point, with: event) } - private func updateIsTranslating(_ isTranslating: Bool) { + public func updateIsTranslating(_ isTranslating: Bool) { guard let item = self.item else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index ba57a1641a2..5fbd7339522 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -55,7 +55,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent return } else { if content.embedUrl == nil && (content.title != nil || content.text != nil) && content.story == nil { - var shouldOpenUrl = true + // MARK: Swiftgram + var shouldOpenUrl = false if let file = content.file { if file.isVideo { shouldOpenUrl = false diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 7c897a228af..79e1ac76aa1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -65,9 +65,9 @@ public final class ChatRecentActionsController: TelegramBaseController { }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) - }, forwardSelectedMessages: { + }, forwardSelectedMessages: { _ in }, forwardCurrentForwardMessages: { - }, forwardMessages: { _ in + }, forwardMessages: { _, _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in }, presentReplyOptions: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 26551a1cb6e..e0014372f63 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1036,7 +1036,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { messageEntities = attribute.entities } if let attribute = attribute as? RestrictedContentMessageAttribute { - restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 3f2527c6e76..564c7608e6d 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -169,6 +169,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol } public let openMessage: (Message, OpenMessageParams) -> Bool + // MARK: Swiftgram + public let sgStartMessageEdit: (Message) -> Void public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void public let openPeerMention: (String, Promise?) -> Void public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void @@ -300,6 +302,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public init( openMessage: @escaping (Message, OpenMessageParams) -> Bool, + sgStartMessageEdit: @escaping (Message) -> Void = { _ in }, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void, openPeerMention: @escaping (String, Promise?) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void, @@ -410,6 +413,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol presentationContext: ChatPresentationContext ) { self.openMessage = openMessage + self.sgStartMessageEdit = sgStartMessageEdit self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 71c184ae51d..09d66a1eea3 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -471,7 +472,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false) { self.context = context self.currentInputData = currentInputData - self.defaultToEmojiTab = defaultToEmojiTab + self.defaultToEmojiTab = SGSimpleSettings.shared.forceEmojiTab ? true : defaultToEmojiTab self.opaqueTopPanelBackground = opaqueTopPanelBackground self.useOpaqueTheme = useOpaqueTheme self.stateContext = stateContext diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 3e7e6d29041..3e4997af479 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -1,15 +1,23 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + +sgsrc = [ + "//Swiftgram/SGEmojiKeyboardDefaultFirst:SGEmojiKeyboardDefaultFirst" +] + swift_library( name = "EntityKeyboard", module_name = "EntityKeyboard", - srcs = glob([ + srcs = sgsrc + glob([ "Sources/**/*.swift", ]), copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 4a780344e55..b134c4cd6b7 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -586,8 +586,8 @@ public final class EmojiPagerContentComponent: Component { public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let inputInteractionHolder: InputInteractionHolder - public let panelItemGroups: [ItemGroup] - public let contentItemGroups: [ItemGroup] + public var panelItemGroups: [ItemGroup] + public var contentItemGroups: [ItemGroup] public let itemLayoutType: ItemLayoutType public let itemContentUniqueId: ContentId? public let searchState: SearchState diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ee7a06c1e05..d3757837bdc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -567,6 +568,11 @@ public final class EntityKeyboardComponent: Component { let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() if let emojiContent = component.emojiContent { + // MARK: Swiftgram + if SGSimpleSettings.shared.defaultEmojisFirst { + emojiContent.panelItemGroups = sgPatchEmojiKeyboardItems(emojiContent.panelItemGroups) + emojiContent.contentItemGroups = sgPatchEmojiKeyboardItems(emojiContent.contentItemGroups) + } contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in emojiContent.panelItemGroups { diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD b/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD index 4ffab8aeb76..2bc571eaa73 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "LegacyInstantVideoController", module_name = "LegacyInstantVideoController", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index b6b6b4ffdf0..d58bc3c73db 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -1,3 +1,6 @@ +// MARK: Swiftgram +import SGSimpleSettings + import Foundation import UIKit import AsyncDisplayKit @@ -164,7 +167,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo let node = ChatSendButtonRadialStatusView(color: theme.chat.inputPanel.panelControlAccentColor) node.slowmodeState = slowmodeState return node - }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)! + }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId, startWithRearCam: SGSimpleSettings.shared.startTelescopeWithRearCam)! controller.presentScheduleController = { done in presentSchedulePicker { time in done?(time) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 828e7500d8f..444b3436d8f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -109,6 +109,7 @@ public enum MediaQualityPreset: Int32 { case compressedVeryHigh case animation case videoMessage + case videoMessageHD case profileLow case profile case profileHigh @@ -137,6 +138,8 @@ public enum MediaQualityPreset: Int32 { return 1280.0 case .compressedVeryHigh: return 1920.0 + case .videoMessageHD: + return 384.0 case .videoMessage: return 400.0 case .profileLow: @@ -162,6 +165,8 @@ public enum MediaQualityPreset: Int32 { return 3000 case .compressedVeryHigh: return 6600 + case .videoMessageHD: + return 2000 case .videoMessage: return 1000 case .profileLow: @@ -182,9 +187,9 @@ public enum MediaQualityPreset: Int32 { var audioBitrateKbps: Int { switch self { case .compressedVeryLow, .compressedLow: - return 32 + return 128 case .compressedMedium, .compressedHigh, .compressedVeryHigh, .videoMessage: - return 64 + return 320 default: return 0 } @@ -1581,7 +1586,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat var values = values var videoBitrate: Int = 3700 - var audioBitrate: Int = 64 + var audioBitrate: Int = 320 var audioNumberOfChannels = 2 if image { videoBitrate = 5000 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e74d17b3909..5f12060b333 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "MediaEditorScreen", module_name = "MediaEditorScreen", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/Postbox:Postbox", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index aee5cbbcb91..19bd03a3216 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -184,7 +185,7 @@ public extension MediaEditorScreen { defer { TempBox.shared.dispose(tempFile) } - if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { + if let imageData = compressImageToJPEG(image, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) { update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil) |> deliverOnMainQueue).startStrict(next: { result in switch result { @@ -223,7 +224,7 @@ public extension MediaEditorScreen { defer { TempBox.shared.dispose(tempFile) } - let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } + let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) } let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in let file = TempBox.shared.tempFile(fileName: "image.jpg") if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index af19ba5bb0e..ba39d7fd8e4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -1,5 +1,16 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSettingsUI:SGSettingsUI", + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + + "//Swiftgram/SGRegDate:SGRegDate", + "//Swiftgram/SGRegDateScheme:SGRegDateScheme", + "//Swiftgram/SGDebugUI:SGDebugUI", +] + + swift_library( name = "PeerInfoScreen", module_name = "PeerInfoScreen", @@ -9,7 +20,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/AccountContext", "//submodules/AccountUtils", "//submodules/ActionSheetPeerItem", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 692c16fd517..c7ea9ac5124 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1,3 +1,5 @@ +import SGRegDateScheme +import SGRegDate import Foundation import UIKit import Postbox @@ -351,6 +353,8 @@ final class PeerInfoPersonalChannelData: Equatable { } final class PeerInfoScreenData { + let regDate: RegDate? + let channelCreationTimestamp: Int32? let peer: Peer? let chatPeer: Peer? let savedMessagesPeer: Peer? @@ -398,6 +402,8 @@ final class PeerInfoScreenData { } init( + regDate: RegDate? = nil, + channelCreationTimestamp: Int32? = nil, peer: Peer?, chatPeer: Peer?, savedMessagesPeer: Peer?, @@ -434,6 +440,8 @@ final class PeerInfoScreenData { profileGiftsContext: ProfileGiftsContext?, premiumGiftOptions: [PremiumGiftCodeOption] ) { + self.regDate = regDate + self.channelCreationTimestamp = channelCreationTimestamp self.peer = peer self.chatPeer = chatPeer self.savedMessagesPeer = savedMessagesPeer @@ -894,6 +902,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, var enableQRLogin = false let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) + // MARK: Swiftgram + if let appConfiguration, appConfiguration.sgWebSettings.global.qrLogin { + enableQRLogin = true + } if let appConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR { enableQRLogin = true } @@ -1271,6 +1283,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return combineLatest( + Signal.single(nil) |> then (getRegDate(context: context, peerId: peerId.id._internalGetInt64Value())), context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), @@ -1289,7 +1302,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueContextAndState, premiumGiftOptions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in + |> map { regDate, peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1373,6 +1386,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return PeerInfoScreenData( + regDate: regDate, peer: peer, chatPeer: peerView.peers[peerId], savedMessagesPeer: savedMessagesPeer?._asPeer(), @@ -1514,6 +1528,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return combineLatest( + getFirstMessage(context: context, peerId: peerId), context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), @@ -1532,7 +1547,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueContextAndState, revenueContextAndState ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState -> PeerInfoScreenData in + |> map { firstMessage, peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -1584,6 +1599,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return PeerInfoScreenData( + channelCreationTimestamp: firstMessage?.timestamp, peer: peerView.peers[peerId], chatPeer: peerView.peers[peerId], savedMessagesPeer: nil, @@ -1798,6 +1814,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let isPremiumRequiredForStoryPosting: Signal = isPremiumRequiredForStoryPosting(context: context) return combineLatest(queue: .mainQueue(), + Signal.single(nil) |> then (getFirstMessage(context: context, peerId: peerId)), context.account.viewTracker.peerView(groupId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder), context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()), @@ -1816,7 +1833,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags, isPremiumRequiredForStoryPosting ) - |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> Signal in + |> mapToSignal { firstMessage, peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> Signal in var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer @@ -1885,7 +1902,24 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + // MARK: Swiftgram + var channelCreationTimestamp = firstMessage?.timestamp + if groupId.namespace == Namespaces.Peer.CloudChannel, let firstMessage { + for media in firstMessage.media { + if let action = media as? TelegramMediaAction { + if case let .channelMigratedFromGroup(_, legacyGroupId) = action.action { + if let legacyGroup = firstMessage.peers[legacyGroupId] as? TelegramGroup { + if legacyGroup.creationDate != 0 { + channelCreationTimestamp = legacyGroup.creationDate + } + } + } + } + } + } + return .single(PeerInfoScreenData( + channelCreationTimestamp: channelCreationTimestamp, peer: peerView.peers[groupId], chatPeer: peerView.peers[groupId], savedMessagesPeer: nil, @@ -2328,3 +2362,20 @@ private func isPremiumRequiredForStoryPosting(context: AccountContext) -> Signal } ) } + + +// MARK: Swiftgram +private func getFirstMessage(context: AccountContext, peerId: PeerId) -> Signal { + return context.engine.messages.getMessagesLoadIfNecessary([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1)]) + |> `catch` { _ in + return .single(.result([])) + } + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> map { $0.first } +} + diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift index 1d2a7e3d41a..37ed27126e4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -95,7 +95,7 @@ private final class MoreIconNode: ManagedAnimationNode { final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { let containerNode: ContextControllerSourceNode let contextSourceNode: ContextReferenceContentNode - private let textNode: ImmediateTextNode + public let textNode: ImmediateTextNode private let iconNode: ASImageNode private let backIconLayer: SimpleShapeLayer private var animationNode: MoreIconNode? @@ -104,7 +104,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { private var key: PeerInfoHeaderNavigationButtonKey? private var contentsColor: UIColor = .white - private var canBeExpanded: Bool = false + public private(set) var canBeExpanded: Bool = false var action: ((ASDisplayNode, ContextGesture?) -> Void)? diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift index 8d1a1aef595..64cfbe6b012 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -97,6 +97,14 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: buttonY), size: buttonSize) nextButtonOrigin += buttonSize.width + 4.0 + // MARK: Swiftgram + if case .back = spec.key { + if buttonNode.canBeExpanded { + nextButtonOrigin += buttonNode.textNode.bounds.size.width + } else { + nextButtonOrigin += buttonSize.width + } + } if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { @@ -146,6 +154,14 @@ final class PeerInfoHeaderNavigationButtonContainerNode: SparseNode { } let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin, y: buttonY), size: buttonSize) nextButtonOrigin += buttonSize.width + 4.0 + // MARK: Swiftgram + if case .back = spec.key { + if buttonNode.canBeExpanded { + nextButtonOrigin += buttonNode.textNode.bounds.size.width + } else { + nextButtonOrigin += buttonSize.width + } + } if spec.isForExpandedView { nextExpandedButtonOrigin = nextButtonOrigin } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 15b5abe9894..a8113958f3b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -79,6 +79,8 @@ private let TitleNodeStateExpanded = 1 final class PeerInfoHeaderNode: ASDisplayNode { private var context: AccountContext private let isPremiumDisabled: Bool + + private var hidePhoneInSettings: Bool private weak var controller: PeerInfoScreenImpl? private var presentationData: PresentationData? private var state: PeerInfoState? @@ -180,8 +182,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var validLayout: (width: CGFloat, deviceMetrics: DeviceMetrics)? - init(context: AccountContext, controller: PeerInfoScreenImpl, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, isMyProfile: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) { + init(hidePhoneInSettings: Bool, context: AccountContext, controller: PeerInfoScreenImpl, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, isMyProfile: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) { self.context = context + self.hidePhoneInSettings = hidePhoneInSettings self.controller = controller self.isAvatarExpanded = avatarInitiallyExpanded self.isOpenedFromChat = isOpenedFromChat @@ -1040,8 +1043,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { title = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } title = title.replacingOccurrences(of: "\u{1160}", with: "").replacingOccurrences(of: "\u{3164}", with: "") + // MARK: Swiftgram if title.isEmpty { - if let peer = peer as? TelegramUser, let phone = peer.phone { + if let peer = peer as? TelegramUser, let phone = peer.phone, !self.hidePhoneInSettings { title = formatPhoneNumber(context: self.context, number: phone) } else if let addressName = peer.addressName { title = "@\(addressName)" @@ -1055,10 +1059,20 @@ final class PeerInfoHeaderNode: ASDisplayNode { smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: .white, shadowColor: titleShadowColor) if self.isSettings, let user = peer as? TelegramUser { - var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "") - + // MARK: Swiftgram + var formattedPhone = formatPhoneNumber(context: self.context, number: user.phone ?? "") + if !formattedPhone.isEmpty && self.hidePhoneInSettings { + formattedPhone = "" + } + + var subtitle = formattedPhone + if let mainUsername = user.addressName, !mainUsername.isEmpty { - subtitle = "\(subtitle) • @\(mainUsername)" + if !subtitle.isEmpty { + subtitle = "\(subtitle) • @\(mainUsername)" + } else { + subtitle = "@\(mainUsername)" + } } subtitleStringText = subtitle subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 1caa1447d76..7570d7a5fba 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1,3 +1,8 @@ +// MARK: Swiftgram +import SGDebugUI +import SGSimpleSettings +import SGSettingsUI +import SGStrings import Foundation import UIKit import Display @@ -325,10 +330,10 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) - }, forwardSelectedMessages: { + }, forwardSelectedMessages: { _ in forwardMessages() }, forwardCurrentForwardMessages: { - }, forwardMessages: { _ in + }, forwardMessages: { _, _ in }, updateForwardOptionsState: { _ in }, presentForwardOptions: { _ in }, presentReplyOptions: { _ in @@ -486,6 +491,8 @@ private enum PeerInfoMemberAction { } private enum PeerInfoContextSubject { + case copy(String) + case aboutDC case bio case phone(String) case link(customLink: String?) @@ -495,6 +502,7 @@ private enum PeerInfoContextSubject { } private enum PeerInfoSettingsSection { + case swiftgram case avatar case edit case proxy @@ -542,6 +550,7 @@ private enum TopicsLimitedReason { } private final class PeerInfoInteraction { + let notifyTextCopied: () -> Void let openChat: (EnginePeer.Id?) -> Void let openUsername: (String, Bool, Promise?) -> Void let openPhone: (String, ASDisplayNode, ContextGesture?, Promise?) -> Void @@ -609,6 +618,7 @@ private final class PeerInfoInteraction { let getController: () -> ViewController? init( + notifyTextCopied: @escaping () -> Void, openUsername: @escaping (String, Bool, Promise?) -> Void, openPhone: @escaping (String, ASDisplayNode, ContextGesture?, Promise?) -> Void, editingOpenNotificationSettings: @escaping () -> Void, @@ -675,6 +685,7 @@ private final class PeerInfoInteraction { openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, getController: @escaping () -> ViewController? ) { + self.notifyTextCopied = notifyTextCopied self.openUsername = openUsername self.openPhone = openPhone self.editingOpenNotificationSettings = editingOpenNotificationSettings @@ -742,9 +753,9 @@ private final class PeerInfoInteraction { self.getController = getController } } - +// MARK: Swiftgram private let enabledPublicBioEntities: EnabledEntityTypes = [.allUrl, .mention, .hashtag] -private let enabledPrivateBioEntities: EnabledEntityTypes = [.internalUrl, .mention, .hashtag] +private let enabledPrivateBioEntities: EnabledEntityTypes = [.allUrl, .mention, .hashtag] private enum SettingsSection: Int, CaseIterable { case edit @@ -752,6 +763,7 @@ private enum SettingsSection: Int, CaseIterable { case accounts case myProfile case proxy + case swiftgram case apps case shortcuts case advanced @@ -760,7 +772,7 @@ private enum SettingsSection: Int, CaseIterable { case support } -private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, isExpanded: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func settingsItems(showProfileId: Bool, data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, isExpanded: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] } @@ -812,6 +824,28 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) } + // MARK: Swiftgram + if showProfileId { + var idText = "" + + if let user = data.peer as? TelegramUser { + idText = String(user.id.id._internalGetInt64Value()) + } + + items[.edit]!.append( + PeerInfoScreenActionItem( + id: 100, + text: "ID: \(idText)", + color: .accent, + action: { + UIPasteboard.general.string = idText + + interaction.notifyTextCopied() + } + ) + ) + } + if let settings = data.globalSettings { if settings.premiumGracePeriod { items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: "Your access to Telegram Premium will expire soon!", text: .markdown("Unfortunately, your latest payment didn't come through. To keep your access to exclusive features, please renew the subscription."), isWarning: true, linkAction: nil)) @@ -903,6 +937,23 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } + let sgSectionId = 0 + // let locale = presentationData.strings.baseLanguageCode + // MARK: Swiftgram + let hasNewSGFeatures = { + return false + } + let swiftgramLabel: PeerInfoScreenDisclosureItem.Label + if hasNewSGFeatures() { + swiftgramLabel = .titleBadge(presentationData.strings.Settings_New, presentationData.theme.list.itemAccentColor) + } else { + swiftgramLabel = .none + } + + items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: sgSectionId, label: swiftgramLabel, text: "Swiftgram", icon: PresentationResourcesSettings.swiftgram, action: { + interaction.openSettings(.swiftgram) + })) + var appIndex = 1000 if let settings = data.globalSettings { for bot in settings.bots { @@ -999,9 +1050,12 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { interaction.openSettings(.businessSetup) })) + // MARK: Swiftgram + /* items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_SendGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) + */ } if let settings = data.globalSettings { @@ -1201,6 +1255,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat } private enum InfoSection: Int, CaseIterable { + case swiftgram case groupLocation case calls case personalChannel @@ -1209,12 +1264,18 @@ private enum InfoSection: Int, CaseIterable { case peerMembers } -private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func infoItems(nearestChatParticipant: (String?, Int32?), showProfileId: Bool, data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] } var currentPeerInfoSection: InfoSection = .peerInfo + + // MARK: Swiftgram + var sgItemId = 0 + var idText = "" + // var isUser = false + // let lang = presentationData.strings.baseLanguageCode var items: [InfoSection: [PeerInfoScreenItem]] = [:] for section in InfoSection.allCases { @@ -1238,6 +1299,10 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let user = data.peer as? TelegramUser { + // MARK: Swiftgram + idText = String(user.id.id._internalGetInt64Value()) +// isUser = true + if !callMessages.isEmpty { items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages)) } @@ -1270,7 +1335,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { label = presentationData.strings.ContactInfo_PhoneLabelMobile } - items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node, progress in + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, additionalText: user.flags.contains(.mutualContact) ? i18n("MutualContact.Label", presentationData.strings.baseLanguageCode) : nil, textColor: .accent, action: { node, progress in interaction.openPhone(phone, node, nil, progress) }, longTapAction: nil, contextAction: { node, gesture, _ in interaction.openPhone(phone, node, gesture, nil) @@ -1541,6 +1606,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } } } else if let channel = data.peer as? TelegramChannel { + // MARK: Swiftgram + idText = "-100" + String(channel.id.id._internalGetInt64Value()) + let ItemUsername = 1 let ItemUsernameInfo = 2 let ItemAbout = 3 @@ -1747,6 +1815,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } } } else if let group = data.peer as? TelegramGroup { + // MARK: Swiftgram + idText = String(group.id.id._internalGetInt64Value()) + if let cachedData = data.cachedData as? CachedGroupData { let aboutText: String? if group.isFake { @@ -1817,6 +1888,96 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } } + // MARK: Swiftgram + if showProfileId { + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: "id: \(idText)", text: "", textColor: .primary, action: nil, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.copy(idText), sourceNode, nil) + }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + + if SGSimpleSettings.shared.showDC { + var dcText = "" + if let peer = data.peer, let smallProfileImage = peer.smallProfileImage, let cloudResource = smallProfileImage.resource as? CloudPeerPhotoSizeMediaResource { + + let dcId = cloudResource.datacenterId + dcText = "\(cloudResource.datacenterId)" + let dcLocation: String? + switch (dcId) { + case 1: + dcLocation = "Miami" + case 2: + dcLocation = "Amsterdam" + case 3: + dcLocation = "Miami" + case 4: + dcLocation = "Amsterdam" + case 5: + dcLocation = "Singapore" + default: + dcLocation = nil + } + + if let dcLocation = dcLocation { + dcText += " \(dcLocation)" + } + } + if !dcText.isEmpty { + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: "dc: \(dcText)", text: "", textColor: .primary, action: nil, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.aboutDC, sourceNode, nil) + }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + } + + if SGSimpleSettings.shared.showCreationDate { + if let channelCreationTimestamp = data.channelCreationTimestamp { + let creationDateString = stringForDate(timestamp: channelCreationTimestamp, strings: presentationData.strings) + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.Created", presentationData.strings.baseLanguageCode, creationDateString), text: "", action: nil, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.copy(creationDateString), sourceNode, nil) + }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + } + + if let invitedAt = nearestChatParticipant.1 { + let joinedDateString = stringForDate(timestamp: invitedAt, strings: presentationData.strings) + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.JoinedDateTitle", presentationData.strings.baseLanguageCode, nearestChatParticipant.0 ?? "chat") , text: joinedDateString, action: nil, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.copy(joinedDateString), sourceNode, nil) + }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + + if SGSimpleSettings.shared.showRegDate { + if let regDate = data.regDate { + let regTimestamp = Int32((regDate.from + regDate.to) / 2) + let regDateString: String + switch (context.currentAppConfiguration.with { $0 }.sgWebSettings.global.regdateFormat) { + case "year": + regDateString = stringForDateWithoutDayAndMonth(date: Date(timeIntervalSince1970: Double(regTimestamp)), strings: presentationData.strings) + case "month": + regDateString = stringForDateWithoutDay(date: Date(timeIntervalSince1970: Double(regTimestamp)), strings: presentationData.strings) + default: + regDateString = stringForDate(timestamp: regTimestamp, strings: presentationData.strings) + } + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.RegDate", presentationData.strings.baseLanguageCode), text: regDateString, action: nil, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.copy(regDateString), sourceNode, nil) + }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + } + + var result: [(AnyHashable, [PeerInfoScreenItem])] = [] for section in InfoSection.allCases { if let sectionItems = items[section], !sectionItems.isEmpty { @@ -2553,7 +2714,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private let callMessages: [Message] private let chatLocation: ChatLocation private let chatLocationContextHolder: Atomic - + let isSettings: Bool let isMyProfile: Bool private let isMediaOnly: Bool @@ -2597,6 +2758,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private let enqueueMediaMessageDisposable = MetaDisposable() private(set) var validLayout: (ContainerViewLayout, CGFloat)? + private(set) var nearestChatParticipant: (String?, Int32?) = (nil, nil) + private(set) var showProfileId: Bool = SGUISettings.default.showProfileId private(set) var data: PeerInfoScreenData? private(set) var state = PeerInfoState( isEditing: false, @@ -2675,7 +2838,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private var didSetReady = false - init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, starsContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, initialPaneKey: PeerInfoPaneKey?) { + init(hidePhoneInSettings: Bool, controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, starsContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, initialPaneKey: PeerInfoPaneKey?) { self.controller = controller self.context = context self.peerId = peerId @@ -2700,7 +2863,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if case let .replyThread(message) = chatLocation { forumTopicThreadId = message.threadId } - self.headerNode = PeerInfoHeaderNode(context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, isMyProfile: isMyProfile, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation) + self.headerNode = PeerInfoHeaderNode(hidePhoneInSettings: hidePhoneInSettings, context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, isMyProfile: isMyProfile, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation) self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey) super.init() @@ -2708,6 +2871,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.parentController = controller self._interaction = PeerInfoInteraction( + notifyTextCopied: { [weak self] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + }, openUsername: { [weak self] value, isMainUsername, progress in self?.openUsername(value: value, isMainUsername: isMainUsername, progress: progress) }, @@ -4246,7 +4413,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { }, completion: nil) } - (strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.3, curve: .linear)) + (strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(SGSimpleSettings.shared.hideTabBar ? true : false, transition: .animated(duration: 0.3, curve: .linear)) case .select: strongSelf.state = strongSelf.state.withSelectedMessageIds(Set()) if let (layout, navigationHeight) = strongSelf.validLayout { @@ -4566,14 +4733,35 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.updateNavigation(transition: .immediate, additive: true, animateHeader: true) } + // MARK: Swiftgram + let showProfileIdSignal = self.context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings]) + |> map { view -> Bool in + let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default + return settings.showProfileId + } + |> distinctUntilChanged + let nearestChatParticipantSignal = .single((nil, nil)) |> then(self.fetchNearestChatParticipant()) |> distinctUntilChanged { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + } + self.dataDisposable = combineLatest( queue: Queue.mainQueue(), + nearestChatParticipantSignal, + showProfileIdSignal, screenData, self.forceIsContactPromise.get() - ).startStrict(next: { [weak self] data, forceIsContact in + ).startStrict(next: { [weak self] nearestChatParticipant, showProfileId, data, forceIsContact in guard let strongSelf = self else { return } + strongSelf.nearestChatParticipant = nearestChatParticipant + strongSelf.showProfileId = showProfileId if data.isContact && forceIsContact { strongSelf.forceIsContactPromise.set(false) } else { @@ -6098,7 +6286,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + // MARK: Swiftgram + if ({ return false }()) && strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -7284,7 +7473,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) let (canTranslate, language) = canTranslateText(context: self.context, text: bioText, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) - if canTranslate { + if canTranslate || { return true }() { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuTranslate, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss { guard let self else { @@ -8891,6 +9080,42 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } })) } + // MARK: Swiftgram + case let .copy(text): + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in + UIPasteboard.general.string = text + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0) + if let sourceRect = sourceRect { + rect = sourceRect.insetBy(dx: 0.0, dy: 2.0) + } + return (sourceNode, rect, controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + case .aboutDC: + let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Passport_InfoLearnMore, accessibilityLabel: self.presentationData.strings.Passport_InfoLearnMore), action: { [weak self] in + self?.openUrl(url: "https://core.telegram.org/api/datacenter", concealed: false, external: false) + + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0) + if let sourceRect = sourceRect { + rect = sourceRect.insetBy(dx: 0.0, dy: 2.0) + } + return (sourceNode, rect, controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + case .bio: var text: String? if let cachedData = data.cachedData as? CachedUserData { @@ -8919,7 +9144,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })] let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) - if canTranslate { + if canTranslate || { return true }() { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuTranslate, accessibilityLabel: presentationData.strings.Conversation_ContextMenuTranslate), action: { [weak self] in let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages) @@ -10027,6 +10252,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var updatedControllers = navigationController.viewControllers for controller in navigationController.viewControllers.reversed() { if controller !== strongSelf && !(controller is TabBarController) { + if SGSimpleSettings.shared.hideTabBar { break } updatedControllers.removeLast() } else { break @@ -10042,6 +10268,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } switch section { + case .swiftgram: + self.controller?.push(sgSettingsController(context: self.context)) case .avatar: self.openAvatarForEditing() case .edit: @@ -10206,15 +10434,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - var maximumAvailableAccounts: Int = 3 + var maximumAvailableAccounts: Int = maximumSwiftgramNumberOfAccounts if accountAndPeer?.1.isPremium == true && !strongSelf.context.account.testingEnvironment { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } var count: Int = 1 for (accountContext, peer, _) in accountsAndPeers { if !accountContext.account.testingEnvironment { if peer.isPremium { - maximumAvailableAccounts = 4 + maximumAvailableAccounts = maximumSwiftgramNumberOfAccounts } count += 1 } @@ -10234,7 +10462,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro navigationController.pushViewController(controller) } } else { - strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment) + if count + 1 > maximumSafeNumberOfAccounts { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: strongSelf.context, title: presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: i18n("Auth.AccountBackupReminder", presentationData.strings.baseLanguageCode), actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment) + }) + ], dismissOnOutsideTap: false) + strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(alertController) + } else { + strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment) + } } }) case .logout: @@ -10873,7 +11111,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro searchDisplayController.deactivate(placeholder: nil) if self.isSettings { - (self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.3, curve: .linear)) + (self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(SGSimpleSettings.shared.hideTabBar ? true : false, transition: .animated(duration: 0.3, curve: .linear)) } let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut) @@ -11438,7 +11676,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro insets.left += sectionInset insets.right += sectionInset - let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) + let items = self.isSettings ? settingsItems(showProfileId: self.showProfileId, data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(nearestChatParticipant: self.nearestChatParticipant, showProfileId: self.showProfileId, data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) contentHeight += headerHeight if !((self.isSettings || self.isMyProfile) && self.state.isEditing) { @@ -11849,6 +12087,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else { if self.isSettings { leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + if SGSimpleSettings.shared.hideTabBar { leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .back, isForExpandedView: false)) } rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) } else if self.isMyProfile { @@ -12276,6 +12515,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var tabBarItemDisposable: Disposable? + private let hidePhoneInSettings: Bool + fileprivate var controllerNode: PeerInfoScreenNode { return self.displayNode as! PeerInfoScreenNode } @@ -12315,8 +12556,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false) { + public init(hidePhoneInSettings: Bool = SGSimpleSettings.defaultValues[SGSimpleSettings.Keys.hidePhoneInSettings.rawValue] as! Bool, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false) { self.context = context + self.hidePhoneInSettings = hidePhoneInSettings self.updatedPresentationData = updatedPresentationData self.peerId = peerId self.avatarInitiallyExpanded = avatarInitiallyExpanded @@ -12676,7 +12918,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } else if self.switchToGifts { initialPaneKey = .gifts } - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey) + self.displayNode = PeerInfoScreenNode(hidePhoneInSettings: self.hidePhoneInSettings, controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) @@ -12928,6 +13170,22 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc let strings = self.presentationData.strings var items: [ContextMenuItem] = [] + + // MARK: Swiftgram + #if DEBUG + items.append(.action(ContextMenuActionItem(text: "Swiftgram Debug", icon: { theme in + return generateTintedImage(image: nil, color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + guard let self = self else { + return + } + self.push(sgDebugController(context: self.context)) + + f(.dismissWithoutContent) + }))) + #endif + // + items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -14065,3 +14323,91 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + + + +// MARK: Swiftgram +extension PeerInfoScreenImpl { + + public func tabBarItemContextAction(sourceView: UIView, gesture: ContextGesture?) { + guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else { + return + } + + let strings = self.presentationData.strings + + var items: [ContextMenuItem] = [] + + // MARK: Swiftgram + #if DEBUG + items.append(.action(ContextMenuActionItem(text: "Swiftgram Debug", icon: { theme in + return generateTintedImage(image: nil, color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + guard let self = self else { + return + } + self.push(sgDebugController(context: self.context)) + + f(.dismissWithoutContent) + }))) + #endif + // + + items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.openSettings(section: .addAccount) + f(.dismissWithoutContent) + }))) + + + items.append(.custom(AccountPeerContextItem(context: self.context, account: self.context.account, peer: primary.1, action: { _, f in + f(.default) + }), true)) + + if !other.isEmpty { + items.append(.separator) + } + + for account in other { + let id = account.0.account.id + items.append(.custom(AccountPeerContextItem(context: self.context, account: account.0.account, peer: account.1, action: { [weak self] _, f in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.switchToAccount(id: id) + f(.dismissWithoutContent) + }), true)) + } + + let controller = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) + } +} + +extension PeerInfoScreenNode { + + public func fetchNearestChatParticipant() -> Signal<(String?, Int32?), NoError> { + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return .single((nil, nil)) + } + + for controller in navigationController.viewControllers.reversed() { + if let chatController = controller as? ChatController, let chatPeerId = chatController.chatLocation.peerId, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(chatPeerId.namespace) { + return self.context.engine.peers.fetchChannelParticipant(peerId: chatPeerId, participantId: self.peerId) + |> mapToSignal { participant -> Signal<(String?, Int32?), NoError> in + if let participant = participant, case let .member(_, invitedAt, _, _, _, _) = participant { + return .single((chatController.overlayTitle, invitedAt)) + } else { + return .single((nil, nil)) + } + } + + } + } + return .single((nil, nil)) + } +} diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 934bc527779..5974c93632d 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -347,9 +347,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) - }, forwardSelectedMessages: { + }, forwardSelectedMessages: { _ in }, forwardCurrentForwardMessages: { - }, forwardMessages: { _ in + }, forwardMessages: { _, _ in }, updateForwardOptionsState: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState(f($0.forwardOptionsState ?? ChatInterfaceForwardOptionsState(hideNames: false, hideCaptions: false, unhideNamesOnCaptionChange: false))) }) }) diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index 357e5a00adb..e8996119158 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -850,6 +850,8 @@ private extension MediaQualityPreset { qualityPreset = .animation case TGMediaVideoConversionPresetVideoMessage: qualityPreset = .videoMessage + case TGMediaVideoConversionPresetVideoMessageHD: + qualityPreset = .videoMessageHD default: qualityPreset = .compressedMedium } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 52d550b52c0..fec4ae8371d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -562,7 +562,7 @@ final class StarsTransactionsScreenComponent: Component { count: self.starsState?.balance ?? 0, rate: nil, actionTitle: environment.strings.Stars_Intro_Buy, - actionAvailable: !premiumConfiguration.areStarsDisabled, + actionAvailable: false, /* MARK: Swiftgram */ // !premiumConfiguration.areStarsDisabled, actionIsEnabled: true, action: { [weak self] in guard let self, let component = self.component else { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift index f59150b4d1e..86445a29263 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift @@ -32,14 +32,15 @@ private func totalDiskSpace() -> Int64 { } } +// MARK: Swiftgram private let maximumCacheSizeValues: [Int32] = { let diskSpace = totalDiskSpace() if diskSpace > 100 * 1024 * 1024 * 1024 { - return [5, 20, 50, Int32.max] + return [1, 5, 20, 50, Int32.max] } else if diskSpace > 50 * 1024 * 1024 * 1024 { - return [5, 16, 32, Int32.max] + return [1, 5, 16, 32, Int32.max] } else if diskSpace > 24 * 1024 * 1024 * 1024 { - return [2, 8, 16, Int32.max] + return [1, 2, 8, 16, Int32.max] } else { return [1, 4, 8, Int32.max] } @@ -84,7 +85,8 @@ final class StorageKeepSizeComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.titles = (0 ..< 4).map { _ in ComponentView() } + // MARK: Swiftgram + self.titles = (0 ..< 5).map { _ in ComponentView() } super.init(frame: frame) @@ -149,10 +151,10 @@ final class StorageKeepSizeComponent: Component { sliderView.lineSize = 4.0 sliderView.dotSize = 5.0 sliderView.minimumValue = 0.0 - sliderView.maximumValue = 3.0 + sliderView.maximumValue = 4.0 sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true - sliderView.positionsCount = 4 + sliderView.positionsCount = 5 sliderView.useLinesForPositions = true sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) self.sliderView = sliderView @@ -179,8 +181,8 @@ final class StorageKeepSizeComponent: Component { guard let sliderView = self.sliderView, let component = self.component else { return } - sliderView.maximumValue = 3.0 - sliderView.positionsCount = 4 + sliderView.maximumValue = 4.0 + sliderView.positionsCount = 5 let value = maximumCacheSizeValues.firstIndex(where: { $0 == component.value }) ?? 0 sliderView.value = CGFloat(value) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 45352322e28..41a184bba06 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1927,7 +1927,8 @@ final class StorageUsageScreenComponent: Component { guard let self, let component = self.component else { return } - let value = max(5, value) + // MARK: Swiftgram + // let value = max(5, value) let _ = updateCacheStorageSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { current in var current = current current.defaultCacheStorageLimitGigabytes = value @@ -3175,19 +3176,21 @@ final class StorageUsageScreenComponent: Component { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presetValues: [Int32] - + // MARK: Swiftgram if case .stories = mappedCategory { presetValues = [ 7 * 24 * 60 * 60, 2 * 24 * 60 * 60, - 1 * 24 * 60 * 60 + 1 * 24 * 60 * 60, + 1 * 60 * 60 ] } else { presetValues = [ Int32.max, 31 * 24 * 60 * 60, 7 * 24 * 60 * 60, - 1 * 24 * 60 * 60 + 1 * 24 * 60 * 60, + 1 * 60 * 60 ] } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 4d244dd6fc4..f8c93e67487 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -1,5 +1,10 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "StoryContainerScreen", module_name = "StoryContainerScreen", @@ -9,7 +14,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/SGStoryWarnComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/SGStoryWarnComponent.swift new file mode 100644 index 00000000000..0121039cafc --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/SGStoryWarnComponent.swift @@ -0,0 +1,252 @@ +import SGStrings + +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import MultilineTextComponent +import BalancedTextComponent +import TelegramCore +import ButtonComponent + +final class SGStoryWarningComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + let isInStealthMode: Bool + let action: () -> Void + let close: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer? = nil, + isInStealthMode: Bool, + action: @escaping () -> Void, + close: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.peer = peer + self.strings = strings + self.isInStealthMode = isInStealthMode + self.action = action + self.close = close + } + + static func ==(lhs: SGStoryWarningComponent, rhs: SGStoryWarningComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView { + private var component: SGStoryWarningComponent? + private weak var state: EmptyComponentState? + + private let effectView: UIVisualEffectView + private let containerView = UIView() + private let titleLabel = ComponentView() + private let descriptionLabel = ComponentView() + private let actionButton = ComponentView() + + let closeButton: HighlightableButton + + override init(frame: CGRect) { + self.effectView = UIVisualEffectView(effect: nil) + + self.closeButton = HighlightableButton() + + super.init(frame: frame) + + self.addSubview(self.effectView) + self.addSubview(self.containerView) + + self.actionButton.view?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleProceed))) + // Configure closeButton + if let image = UIImage(named: "Stories/Close") { + closeButton.setImage(image, for: .normal) + } + closeButton.addTarget(self, action: #selector(handleClose), for: .touchUpInside) + self.addSubview(closeButton) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleProceed() { + if let component = self.component { + component.action() + } + } + + @objc private func handleClose() { + if let component = self.component { + component.close() + } + } + + var didAnimateOut = false + + func animateIn() { + self.didAnimateOut = false + UIView.animate(withDuration: 0.2) { + self.effectView.effect = UIBlurEffect(style: .dark) + } + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerView.layer.animateScale(from: 0.85, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut(completion: @escaping () -> Void) { + guard !self.didAnimateOut else { + return + } + self.didAnimateOut = true + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerView.layer.animateScale(from: 1.0, to: 1.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + + func update(component: SGStoryWarningComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 48.0 + let topInset: CGFloat = min(48.0, floor(availableSize.width * 0.1)) + let navigationStripTopInset: CGFloat = 15.0 + + let closeButtonSize = CGSize(width: 50.0, height: 64.0) + self.closeButton.frame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width, y: navigationStripTopInset + topInset), size: closeButtonSize) + + var authorName = i18n("Stories.Warning.Author", component.strings.baseLanguageCode) + if let peer = component.peer { + authorName = peer.displayTitle(strings: component.strings, displayOrder: .firstLast) + } + + let titleSize = self.titleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: i18n("Stories.Warning.ViewStory", component.strings.baseLanguageCode), + font: Font.semibold(20.0), + textColor: .white, + paragraphAlignment: .center + )) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let textSize = self.descriptionLabel.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .plain(NSAttributedString( + string: i18n(component.isInStealthMode ? "Stories.Warning.NoticeStealth" : "Stories.Warning.Notice", component.strings.baseLanguageCode, authorName), + font: Font.regular(15.0), + textColor: UIColor(rgb: 0xffffff, alpha: 0.6), + paragraphAlignment: .center + )), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + let buttonSize = self.actionButton.update( + transition: .immediate, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: component.strings.Chat_StoryMentionAction, + component: AnyComponent(ButtonTextContentComponent( + text: component.strings.Chat_StoryMentionAction, + badge: 0, + textColor: component.theme.list.itemCheckColors.foregroundColor, + badgeBackground: component.theme.list.itemCheckColors.foregroundColor, + badgeForeground: component.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + self?.handleProceed() + } + ) + ) + , + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + + + let totalHeight = titleSize.height + 7.0 + textSize.height + 50.0 + buttonSize.height + let originY = (availableSize.height - totalHeight) / 2.0 + + let titleFrame = CGRect( + origin: CGPoint(x: (availableSize.width - titleSize.width) / 2.0, y: originY), + size: titleSize + ) + if let view = self.titleLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = titleFrame + } + + let textFrame = CGRect( + origin: CGPoint(x: (availableSize.width - textSize.width) / 2.0, y: titleFrame.maxY + 7.0), + size: textSize + ) + if let view = self.descriptionLabel.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = textFrame + } + + let buttonFrame = CGRect( + origin: CGPoint(x: (availableSize.width - buttonSize.width) / 2.0, y: textFrame.maxY + 50.0), + size: buttonSize + ) + if let view = self.actionButton.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + view.frame = buttonFrame + } + + let bounds = CGRect(origin: .zero, size: availableSize) + self.effectView.frame = bounds + self.containerView.frame = bounds + + return availableSize + } + + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 4d26f2e7152..6fa3300ee10 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1,3 +1,7 @@ +// MARK: Swiftgram +import TelegramUIPreferences +import SGSimpleSettings + import Foundation import UIKit import Display @@ -426,7 +430,12 @@ private final class StoryContainerScreenComponent: Component { var longPressRecognizer: StoryLongPressRecognizer? private var pendingNavigationToItemId: StoryId? - + + private let storiesWarning = ComponentView() + private var requestedDisplayStoriesWarning: Bool = SGUISettings.default.warnOnStoriesOpen + private var displayStoriesWarningDisposable: Disposable? + private var isDisplayingStoriesWarning: Bool = false + private let interactionGuide = ComponentView() private var isDisplayingInteractionGuide: Bool = false private var displayInteractionGuideDisposable: Disposable? @@ -459,7 +468,7 @@ private final class StoryContainerScreenComponent: Component { guard let self, let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { return [] } - if self.isDisplayingInteractionGuide { + if self.isDisplayingInteractionGuide || self.isDisplayingStoriesWarning { return [] } if let environment = self.environment, case .regular = environment.metrics.widthClass { @@ -592,7 +601,7 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return false } - if self.isDisplayingInteractionGuide { + if self.isDisplayingInteractionGuide || self.isDisplayingStoriesWarning { return false } if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { @@ -745,6 +754,7 @@ private final class StoryContainerScreenComponent: Component { deinit { self.contentUpdatedDisposable?.dispose() + self.displayStoriesWarningDisposable?.dispose() self.volumeButtonsListenerShouldBeActiveDisposable?.dispose() self.headphonesDisposable?.dispose() self.stealthModeDisposable?.dispose() @@ -1064,7 +1074,7 @@ private final class StoryContainerScreenComponent: Component { guard let self else { return } - if !value && !self.isDisplayingInteractionGuide { + if !value && (!self.isDisplayingInteractionGuide || !self.isDisplayingStoriesWarning) { if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View { currentItemView.maybeDisplayReactionTooltip() } @@ -1300,6 +1310,28 @@ private final class StoryContainerScreenComponent: Component { } }) + // MARK: Swiftgram + let warnOnStoriesOpenSignal = component.context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings]) + |> map { view -> Bool in + let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default + return settings.warnOnStoriesOpen + } + |> distinctUntilChanged + + self.displayStoriesWarningDisposable = (warnOnStoriesOpenSignal + |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + self.requestedDisplayStoriesWarning = value + if self.requestedDisplayStoriesWarning { + self.isDisplayingStoriesWarning = true + if update { + self.state?.updated(transition: .immediate) + } + } + }) + update = true } @@ -1357,6 +1389,11 @@ private final class StoryContainerScreenComponent: Component { if case .file = slice.item.storyItem.media { isVideo = true } + // TODO(swiftgram): Show warning on each new peerId story + /* if self.requestedDisplayStoriesWarning, let previousSlice = stateValue?.previousSlice, previousSlice.peer.id != slice.peer.id { + self.isDisplayingStoriesWarning = self.requestedDisplayStoriesWarning + update = false + }*/ } self.focusedItem.set(focusedItemId) self.contentWantsVolumeButtonMonitoring.set(isVideo) @@ -1478,7 +1515,7 @@ private final class StoryContainerScreenComponent: Component { if self.pendingNavigationToItemId != nil { isProgressPaused = true } - if self.isDisplayingInteractionGuide { + if self.isDisplayingInteractionGuide || self.isDisplayingStoriesWarning { isProgressPaused = true } @@ -1906,6 +1943,54 @@ private final class StoryContainerScreenComponent: Component { controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition) } + // MARK: Swiftgram + if self.isDisplayingStoriesWarning { + let _ = self.storiesWarning.update( + transition: .immediate, + component: AnyComponent( + SGStoryWarningComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: component.content.stateValue?.slice?.peer, + isInStealthMode: stealthModeTimeout != nil || SGSimpleSettings.shared.isStealthModeEnabled, + action: { [weak self] in + self?.isDisplayingStoriesWarning = false + self?.state?.updated(transition: .immediate) + if let view = self?.storiesWarning.view as? SGStoryWarningComponent.View { + view.animateOut(completion: { + view.removeFromSuperview() + }) + } + }, + close: { [weak self] in + self?.environment?.controller()?.dismiss() + if let view = self?.storiesWarning.view as? SGStoryWarningComponent.View { + view.animateOut(completion: { + view.removeFromSuperview() + }) + } + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.storiesWarning.view as? SGStoryWarningComponent.View { + if view.superview == nil { + self.addSubview(view) + + view.animateIn() + } + view.layer.zPosition = 1000.0 + view.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let view = self.storiesWarning.view as? StoryInteractionGuideComponent.View, view.superview != nil { + view.animateOut(completion: { + view.removeFromSuperview() + }) + } + if self.isDisplayingInteractionGuide { let _ = self.interactionGuide.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5c26840dfd9..f851e156654 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD b/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD index d39746aa692..0df18ec2983 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgDeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "VideoMessageCameraScreen", module_name = "VideoMessageCameraScreen", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgDeps + [ "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/Postbox", diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index d8d2908bae3..9cc0392c40c 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -891,7 +892,8 @@ public class VideoMessageCameraScreen: ViewController { self.previewContainerView.addSubview(self.previewContainerContentView) let isDualCameraEnabled = Camera.isDualCameraSupported(forRoundVideo: true) - let isFrontPosition = "".isEmpty + // MARK: Swiftgram + let isFrontPosition = !SGSimpleSettings.shared.startTelescopeWithRearCam self.mainPreviewView = CameraSimplePreviewView(frame: .zero, main: true, roundVideo: true) self.additionalPreviewView = CameraSimplePreviewView(frame: .zero, main: false, roundVideo: true) @@ -1553,7 +1555,7 @@ public class VideoMessageCameraScreen: ViewController { private var validLayout: ContainerViewLayout? - fileprivate var camera: Camera? { + public var camera: Camera? { return self.node.camera } diff --git a/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png b/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png index 937fc445406..1cc6ddeffd0 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png and b/submodules/TelegramUI/Images.xcassets/Components/AppBadge.imageset/AppBadge@3x.png differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 2b14a058837..39c21e4b786 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -1,3 +1,6 @@ +import SGStrings +import SGSimpleSettings + import Foundation import SwiftSignalKit import UIKit @@ -655,6 +658,8 @@ public final class AccountContextImpl: AccountContext { } public func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) { + // MARK: Swiftgram + let makeCall = { guard let callResult = self.sharedContext.callManager?.requestCall(context: self, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false) else { return } @@ -722,6 +727,19 @@ public final class AccountContextImpl: AccountContext { } else { completion() } + // MARK: Swiftgram + } + if SGSimpleSettings.shared.confirmCalls { + let presentationData = self.sharedContext.currentPresentationData.with { $0 } + self.sharedContext.mainWindow?.present(textAlertController(context: self, title: nil, text: isVideo ? i18n("CallConfirmation.Video.Title", presentationData.strings.baseLanguageCode) : i18n("CallConfirmation.Audio.Title", presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { [weak self] in + guard let _ = self else { + return + } + makeCall() + })]), on: .root) + } else { + makeCall() + } } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1e853ce12b8..8879ddae969 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1,3 +1,8 @@ +// MARK: Swiftgram +import SGActionRequestHandlerSanitizer +import SGAPIWebSettings +import SGLogging +import SGStrings import UIKit import SwiftSignalKit import Display @@ -828,6 +833,27 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue |> map { accountAndSettings -> AuthorizedApplicationContext? in return accountAndSettings.flatMap { context, callListSettings in - return AuthorizedApplicationContext(sharedApplicationContext: sharedApplicationContext, mainWindow: self.mainWindow, watchManagerArguments: .single(nil), context: context as! AccountContextImpl, accountManager: sharedApplicationContext.sharedContext.accountManager, showCallsTab: callListSettings.showTab, reinitializedNotificationSettings: { + return AuthorizedApplicationContext(sharedApplicationContext: sharedApplicationContext, mainWindow: self.mainWindow, watchManagerArguments: .single(nil), context: context as! AccountContextImpl, accountManager: sharedApplicationContext.sharedContext.accountManager, showContactsTab: callListSettings.showContactsTab, showCallsTab: callListSettings.showTab, reinitializedNotificationSettings: { let _ = (self.context.get() |> take(1) |> deliverOnMainQueue).start(next: { context in @@ -1168,6 +1194,8 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue).start(next: { sharedApplicationContext in @@ -1852,6 +1887,11 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue).start(next: { activeAccounts in for (_, context, _) in activeAccounts.accounts { + // MARK: Swiftgram + updateSGWebSettingsInteractivelly(context: context) + if onlySG { + continue + } (context.downloadedMediaStoreManager as? DownloadedMediaStoreManagerImpl)?.runTasks() } }) @@ -2201,6 +2241,7 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { sharedContext, context, authContext in + let url = sgActionRequestHandlerSanitizer(url) if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(sharedContext: sharedContext, url: url) { authContext.rootController.applyConfirmationCode(confirmationCode) } else if let context = context { diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 89264f0f67e..a9880fc3811 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -1,3 +1,5 @@ +// MARK: Swiftgram +import SGSimpleSettings import Foundation import Intents import TelegramPresentationData @@ -151,11 +153,12 @@ final class AuthorizedApplicationContext { private var applicationInForegroundDisposable: Disposable? + private var showContactsTab: Bool private var showCallsTab: Bool private var showCallsTabDisposable: Disposable? private var enablePostboxTransactionsDiposable: Disposable? - init(sharedApplicationContext: SharedApplicationContext, mainWindow: Window1, watchManagerArguments: Signal, context: AccountContextImpl, accountManager: AccountManager, showCallsTab: Bool, reinitializedNotificationSettings: @escaping () -> Void) { + init(sharedApplicationContext: SharedApplicationContext, mainWindow: Window1, watchManagerArguments: Signal, context: AccountContextImpl, accountManager: AccountManager, showContactsTab: Bool, showCallsTab: Bool, reinitializedNotificationSettings: @escaping () -> Void) { self.sharedApplicationContext = sharedApplicationContext setupLegacyComponents(context: context) @@ -166,11 +169,13 @@ final class AuthorizedApplicationContext { self.context = context + self.showContactsTab = showContactsTab + self.showCallsTab = showCallsTab self.notificationController = NotificationContainerController(context: context) - self.rootController = TelegramRootController(context: context) + self.rootController = TelegramRootController(showTabNames: SGSimpleSettings.shared.showTabNames, context: context) self.rootController.minimizedContainer = self.sharedApplicationContext.minimizedContainer[context.account.id] self.rootController.minimizedContainerUpdated = { [weak self] minimizedContainer in guard let self else { @@ -249,7 +254,7 @@ final class AuthorizedApplicationContext { } if self.rootController.rootTabController == nil { - self.rootController.addRootControllers(showCallsTab: self.showCallsTab) + self.rootController.addRootControllers(hidePhoneInSettings: SGSimpleSettings.shared.hidePhoneInSettings, showContactsTab: self.showContactsTab, showCallsTab: self.showCallsTab) } if let tabsController = self.rootController.viewControllers.first as? TabBarController, !tabsController.controllers.isEmpty, tabsController.selectedIndex >= 0 { let controller = tabsController.controllers[tabsController.selectedIndex] @@ -780,18 +785,28 @@ final class AuthorizedApplicationContext { }) let showCallsTabSignal = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]) - |> map { sharedData -> Bool in - var value = CallListSettings.defaultSettings.showTab + |> map { sharedData -> (Bool, Bool) in + var showCallsTabValue = CallListSettings.defaultSettings.showTab + var showContactsTabValue = CallListSettings.defaultSettings.showContactsTab if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) { - value = settings.showTab + showCallsTabValue = settings.showTab + showContactsTabValue = settings.showContactsTab } - return value + return (showContactsTabValue, showCallsTabValue) } - self.showCallsTabDisposable = (showCallsTabSignal |> deliverOnMainQueue).start(next: { [weak self] value in + self.showCallsTabDisposable = (showCallsTabSignal |> deliverOnMainQueue).start(next: { [weak self] showContactsTabValue, showCallsTabValue in if let strongSelf = self { - if strongSelf.showCallsTab != value { - strongSelf.showCallsTab = value - strongSelf.rootController.updateRootControllers(showCallsTab: value) + var needControllersUpdate = false + if strongSelf.showCallsTab != showCallsTabValue { + needControllersUpdate = true + strongSelf.showCallsTab = showCallsTabValue + } + if strongSelf.showContactsTab != showContactsTabValue { + needControllersUpdate = true + strongSelf.showContactsTab = showContactsTabValue + } + if needControllersUpdate { + strongSelf.rootController.updateRootControllers(showContactsTab: showContactsTabValue, showCallsTab: showCallsTabValue) } } }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 88055808055..8387abaaac2 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -667,7 +667,8 @@ extension ChatControllerImpl { if counterAndTimestamp.0 >= 3 { maybeSuggestPremium = true } - if (isPremium || maybeSuggestPremium) && !isHidden { + // MARK: Swiftgram + if (isPremium || maybeSuggestPremium || true) && !isHidden { return chatTranslationState(context: context, peerId: peerId) |> map { translationState -> ChatPresentationTranslationState? in if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) { @@ -688,6 +689,22 @@ extension ChatControllerImpl { }) } }) + + // MARK: Swiftgram + self.chatLanguagePredictionDisposable = ( + chatTranslationState(context: context, peerId: peerId, forcePredict: true) + |> map { translationState -> ChatPresentationTranslationState? in + if let translationState, !translationState.fromLang.isEmpty { + return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode) + } else { + return nil + } + } + |> distinctUntilChanged).startStrict(next: { [weak self] translationState in + if let strongSelf = self, let translationState = translationState, strongSelf.predictedChatLanguage == nil { + strongSelf.predictedChatLanguage = translationState.fromLang + } + }) } self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, @@ -2069,12 +2086,24 @@ extension ChatControllerImpl { } })) } - }, forwardSelectedMessages: { [weak self] in + }, forwardSelectedMessages: { [weak self] mode in if let strongSelf = self { strongSelf.commitPurposefulAction() if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds) + // MARK: Swiftgram + if let mode = mode { + switch (mode) { + case "toCloud": + strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: false, openCloud: false, resetCurrent: true) + case "hideNames": + strongSelf.forwardMessages(forceHideNames: true, messageIds: forwardMessageIds, options: ChatInterfaceForwardOptionsState(hideNames: true, hideCaptions: false, unhideNamesOnCaptionChange: false)) + default: + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } + } else { + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } } } }, forwardCurrentForwardMessages: { [weak self] in @@ -2084,11 +2113,26 @@ extension ChatControllerImpl { strongSelf.forwardMessages(messageIds: forwardMessageIds, options: strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState, resetCurrent: true) } } - }, forwardMessages: { [weak self] messages in + }, forwardMessages: { [weak self] messages, mode in if let strongSelf = self, !messages.isEmpty { strongSelf.commitPurposefulAction() let forwardMessageIds = messages.map { $0.id }.sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds) + // MARK: Swiftgram + if let mode = mode { + switch (mode) { + case "forwardMessagesToCloudWithNoNamesAndOpen": + strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: true, openCloud: true) + case "forwardMessagesToCloud": + strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: false, openCloud: false) + case "forwardMessagesWithNoNames": + strongSelf.forwardMessages(forceHideNames: true, messageIds: forwardMessageIds, options: ChatInterfaceForwardOptionsState(hideNames: true, hideCaptions: false, unhideNamesOnCaptionChange: false)) + default: + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } + } else { + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } + } }, updateForwardOptionsState: { [weak self] f in if let strongSelf = self { @@ -4856,4 +4900,4 @@ extension ChatControllerImpl { self.displayNodeDidLoad() } -} +} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index ac4358c04a8..b6093c91d35 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Postbox diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index bcb0c8c7f66..851d071102c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -16,6 +16,9 @@ import UrlWhitelist import OpenInExternalAppUI import SafariServices +// MARK: Swiftgram +import ShareController + extension ChatControllerImpl { func openLinkContextMenu(url: String, params: ChatControllerInteraction.LongTapParams) -> Void { guard let message = params.message, let contentNode = params.contentNode else { @@ -92,6 +95,22 @@ extension ChatControllerImpl { } self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) })) + // MARK: Swiftgram + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + guard let self else { + return + } + self.present(ShareController(context: self.context, subject: .url(url), immediateExternalShareOverridingSGBehaviour: false), in: .window(.root)) + })) + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuShare, color: .accent, action: { [weak actionSheet, weak self] in + actionSheet?.dismissAnimated() + guard let self else { + return + } + self.present(ShareController(context: self.context, subject: .url(url), immediateExternalShareOverridingSGBehaviour: true), in: .current) + })) + // if canAddToReadingList { items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -183,6 +202,30 @@ extension ChatControllerImpl { })) ) + // MARK: Swiftgram + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + self.present(ShareController(context: self.context, subject: .url(url), immediateExternalShareOverridingSGBehaviour: false), in: .window(.root)) + })) + ) + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuShare, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + self.present(ShareController(context: self.context, subject: .url(url), immediateExternalShareOverridingSGBehaviour: true), in: .current) + })) + ) + // if canAddToReadingList { items.append( .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: theme.contextMenu.primaryColor) }, action: { _, f in diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index 798f9bf0236..4e4bc8f2d2e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -1,3 +1,7 @@ +// MARK: Swiftgram +import SGSimpleSettings +import TextFormat +import TranslateUI import Foundation import UIKit import AsyncDisplayKit @@ -84,6 +88,47 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return } + // MARK: Swiftgram + let outgoingMessageTranslateToLang = SGSimpleSettings.shared.outgoingLanguageTranslation[SGSimpleSettings.makeOutgoingLanguageTranslationKey(accountId: selfController.context.account.peerId.id._internalGetInt64Value(), peerId: peer.id.id._internalGetInt64Value())] ?? selfController.predictedChatLanguage + + let sgTranslationContext: (outgoingMessageTranslateToLang: String?, translate: (() -> Void)?, changeTranslationLanguage: (() -> ())?) = (outgoingMessageTranslateToLang: outgoingMessageTranslateToLang, translate: { [weak selfController] in + guard let selfController else { return } + let textToTranslate = selfController.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string + let textEntities = selfController.presentationInterfaceState.interfaceState.synchronizeableInputState?.entities ?? [] + if let outgoingMessageTranslateToLang = outgoingMessageTranslateToLang { + let _ = (selfController.context.engine.messages.translate(text: textToTranslate, toLang: outgoingMessageTranslateToLang, entities: textEntities) |> deliverOnMainQueue).start(next: { [weak selfController] translatedTextAndEntities in + guard let selfController, let translatedTextAndEntities else { return } + let newInputText = chatInputStateStringWithAppliedEntities(translatedTextAndEntities.0, entities: translatedTextAndEntities.1) + let newTextInputState = ChatTextInputState(inputText: newInputText, selectionRange: 0 ..< newInputText.length) + selfController.updateChatPresentationInterfaceState(interactive: true, { state in + return state.updatedInterfaceState { interfaceState in + return interfaceState.withUpdatedEffectiveInputState(newTextInputState) + } + }) + }) + } + }, changeTranslationLanguage: { [weak selfController] in + guard let selfController else { return } + let controller = languageSelectionController(translateOutgoingMessage: true, context: selfController.context, forceTheme: selfController.presentationData.theme, fromLanguage: "", toLanguage: selfController.presentationInterfaceState.translationState?.fromLang ?? "", completion: { _, toLang in + guard let peerId = selfController.chatLocation.peerId else { + return + } + var langCode = toLang + if langCode == "nb" { + langCode = "no" + } else if langCode == "pt-br" { + langCode = "pt" + } + + if !toLang.isEmpty { + SGSimpleSettings.shared.outgoingLanguageTranslation[SGSimpleSettings.makeOutgoingLanguageTranslationKey(accountId: selfController.context.account.peerId.id._internalGetInt64Value(), peerId: peerId.id._internalGetInt64Value())] = langCode + } + chatMessageDisplaySendMessageOptions(selfController: selfController, node: node, gesture: gesture) + }) + controller.navigationPresentation = .modal + selfController.push(controller) + }) + if let editMessage = selfController.presentationInterfaceState.interfaceState.editMessage { if editMessages.isEmpty { return @@ -122,6 +167,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no } let controller = makeChatSendMessageActionSheetController( + sgTranslationContext: sgTranslationContext, initialData: initialData, context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, @@ -213,6 +259,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no } let controller = makeChatSendMessageActionSheetController( + sgTranslationContext: sgTranslationContext, initialData: initialData, context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b84d45e5ef2..12840c3a874 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Postbox @@ -582,6 +583,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var translationStateDisposable: Disposable? + var chatLanguagePredictionDisposable: Disposable? + var predictedChatLanguage: String? var premiumGiftSuggestionDisposable: Disposable? var nextChannelToReadDisposable: Disposable? @@ -590,7 +593,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var inviteRequestsContext: PeerInvitationImportersContext? var inviteRequestsDisposable = MetaDisposable() - var overlayTitle: String? { + public var overlayTitle: String? { var title: String? if let threadInfo = self.threadInfo { title = threadInfo.title @@ -1469,6 +1472,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return rect } )) + }, sgStartMessageEdit: { [weak self] message in + if let strongSelf = self { + strongSelf.interfaceInteraction?.setupEditMessage(message.id, { _ in }) + } }, openPeer: { [weak self] peer, navigation, fromMessage, source in var expandAvatar = false if case let .groupParticipant(storyStats, avatarHeaderNode) = source { @@ -5847,7 +5854,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { isRegularChat = true } - if strongSelf.nextChannelToReadDisposable == nil, let peerId = strongSelf.chatLocation.peerId, let customChatNavigationStack = strongSelf.customChatNavigationStack { + if strongSelf.nextChannelToReadDisposable == nil, let peerId = strongSelf.chatLocation.peerId, let customChatNavigationStack = strongSelf.customChatNavigationStack, !SGSimpleSettings.shared.disableScrollToNextChannel { if let index = customChatNavigationStack.firstIndex(of: peerId), index != customChatNavigationStack.count - 1 { let nextPeerId = customChatNavigationStack[index + 1] strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), @@ -5885,7 +5892,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateNextChannelToReadVisibility() }) } - } else if isRegularChat, strongSelf.nextChannelToReadDisposable == nil { + } else if isRegularChat, strongSelf.nextChannelToReadDisposable == nil, !SGSimpleSettings.shared.disableScrollToNextChannel { //TODO:loc optimize let accountPeerId = strongSelf.context.account.peerId strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), @@ -6472,7 +6479,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) - if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil { + if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil, !SGSimpleSettings.shared.disableScrollToNextTopic { strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), strongSelf.context.engine.peers.getNextUnreadForumTopic(peerId: channel.id, topicId: Int32(clamping: replyThreadId)), ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) @@ -7111,6 +7118,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.keepMessageCountersSyncrhonizedDisposable?.dispose() self.keepSavedMessagesSyncrhonizedDisposable?.dispose() self.translationStateDisposable?.dispose() + self.chatLanguagePredictionDisposable?.dispose() self.premiumGiftSuggestionDisposable?.dispose() self.powerSavingMonitoringDisposable?.dispose() self.saveMediaDisposable?.dispose() diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 5157826e77c..a7aa6783420 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -15,7 +15,7 @@ import ReactionSelectionNode import TopMessageReactions extension ChatControllerImpl { - func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false) { + func forwardMessages(forceHideNames: Bool = false, messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false) { let _ = (self.context.engine.data.get(EngineDataMap( messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) )) @@ -23,11 +23,11 @@ extension ChatControllerImpl { let sortedMessages = messages.values.compactMap { $0?._asMessage() }.sorted { lhs, rhs in return lhs.id < rhs.id } - self?.forwardMessages(messages: sortedMessages, options: options, resetCurrent: resetCurrent) + self?.forwardMessages(forceHideNames: forceHideNames, messages: sortedMessages, options: options, resetCurrent: resetCurrent) }) } - func forwardMessages(messages: [Message], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool) { + func forwardMessages(forceHideNames: Bool = false, messages: [Message], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool) { let _ = self.presentVoiceMessageDiscardAlert(action: { var filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled, .doNotSearchMessages] var hasPublicPolls = false @@ -48,7 +48,7 @@ extension ChatControllerImpl { } } var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, hasFilters: true, attemptSelection: { peer, _, reason in + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, filter: filter, hasFilters: true, title: forceHideNames ? self.updatedPresentationData.0.strings.Conversation_ForwardOptions_HideSendersNames : nil, attemptSelection: { peer, _, reason in attemptSelectionImpl?(peer, reason) }, multipleSelection: true, forwardedMessageIds: messages.map { $0.id }, selectForumThreads: true)) let context = self.context @@ -115,7 +115,7 @@ extension ChatControllerImpl { } var attributes: [MessageAttribute] = [] - attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true, hideCaptions: forwardOptions?.hideCaptions == true)) + attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true || (options?.hideNames ?? false), hideCaptions: forwardOptions?.hideCaptions == true)) result.append(contentsOf: messages.map { message -> EnqueueMessage in return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) @@ -269,7 +269,7 @@ extension ChatControllerImpl { } if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil, !isPinnedMessages { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: !hasNotOwnMessages, hideCaptions: false, unhideNamesOnCaptionChange: false)).withoutSelectionState() }).updatedSearch(nil) }) + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: !hasNotOwnMessages || (options?.hideNames ?? false), hideCaptions: false, unhideNamesOnCaptionChange: false)).withoutSelectionState() }).updatedSearch(nil) }) strongSelf.updateItemNodesSearchTextHighlightStates() strongSelf.searchResultsController = nil strongController.dismiss() @@ -289,7 +289,7 @@ extension ChatControllerImpl { let mappedMessages = messages.map { message -> EnqueueMessage in let correlationId = Int64.random(in: Int64.min ... Int64.max) correlationIds.append(correlationId) - return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: [], correlationId: correlationId) + return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: forceHideNames ? [ForwardOptionsMessageAttribute(hideNames: true, hideCaptions: false)] : [], correlationId: correlationId) } let _ = (reactionItems @@ -362,7 +362,7 @@ extension ChatControllerImpl { } let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in - return currentState.withUpdatedForwardMessageIds(messages.map { $0.id }).withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: !hasNotOwnMessages, hideCaptions: false, unhideNamesOnCaptionChange: false)) + return currentState.withUpdatedForwardMessageIds(messages.map { $0.id }).withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: !hasNotOwnMessages || (options?.hideNames ?? false), hideCaptions: false, unhideNamesOnCaptionChange: false)) }) |> deliverOnMainQueue).startStandalone(completed: { if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index d461f2a2d14..ba64537d8fa 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1,4 +1,5 @@ import Foundation +import SGSimpleSettings import UIKit import AsyncDisplayKit import Postbox @@ -1480,8 +1481,25 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var dismissedAccessoryPanelNode: AccessoryPanelNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? - - let inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) + // MARK: Swiftgram + var inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) + if SGSimpleSettings.shared.hideChannelBottomButton { + // We still need the panel for messages multi-select or search. Likely can break in future. + if self.chatPresentationInterfaceState.interfaceState.selectionState != nil || self.chatPresentationInterfaceState.search != nil { + self.inputPanelBackgroundNode.isHidden = false + self.inputPanelBackgroundSeparatorNode.isHidden = false + self.inputPanelBottomBackgroundSeparatorNode.isHidden = false + } else if (inputPanelNodes.primary != nil || inputPanelNodes.secondary != nil) { + // So there should be some panel, but user don't want it. Let's check if our logic will hide it + inputPanelNodes = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentPanel: self.inputPanelNode, currentSecondaryPanel: self.secondaryInputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction, forceHideChannelButton: true) + if inputPanelNodes.primary == nil && inputPanelNodes.secondary == nil { + // Looks like we're eligible to hide the panel, let's remove safe area fill as well + self.inputPanelBackgroundNode.isHidden = true + self.inputPanelBackgroundSeparatorNode.isHidden = true + self.inputPanelBottomBackgroundSeparatorNode.isHidden = true + } + } + } let inputPanelBottomInset = max(insets.bottom, inputPanelBottomInsetTerm) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 502b9a9c3a8..472794e4431 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Postbox @@ -326,6 +327,8 @@ private final class ChatHistoryTransactionOpaqueState { } private func extractAssociatedData( + translateToLanguageSG: String?, + translationSettings: TranslationSettings, chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, @@ -407,7 +410,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, starGifts: starGifts) + return ChatMessageItemAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, starGifts: starGifts) } private extension ChatHistoryLocationInput { @@ -748,6 +751,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.messageTransitionNode = messageTransitionNode self.mode = mode + if SGSimpleSettings.shared.disableSnapDeletionEffect { self.allowDustEffect = false } if let data = context.currentAppConfiguration.with({ $0 }).data { if let _ = data["ios_killswitch_disable_unread_alignment"] { self.enableUnreadAlignment = false @@ -833,7 +837,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } self.translationProcessingManager.process = { [weak self, weak context] messageIds in if let context = context, let toLang = self?.toLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang).startStandalone() + let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), toLang: toLang, viaText: !context.isPremium).startStandalone() } } self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in @@ -1643,6 +1647,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto promises, topicAuthorId, translationState, + self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1), maxReadStoryId, recommendedChannels, audioTranscriptionTrial, @@ -1650,9 +1655,16 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto deviceContactsNumbers, contentSettings, starGifts - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, starGifts in + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, sharedData, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, starGifts in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises + let translationSettings: TranslationSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + translationSettings = current + } else { + translationSettings = TranslationSettings.defaultSettings + } + func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -1853,10 +1865,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto displayForNotConsumed: suggestAudioTranscription.1, providedByGroupBoost: audioTranscriptionProvidedByBoost ) - - var translateToLanguage: String? - if let translationState, isPremium && translationState.isEnabled { - var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode + // MARK: Swiftgram + var languageCode = translationState?.toLang ?? chatPresentationData.strings.baseLanguageCode let rawSuffix = "-raw" if languageCode.hasSuffix(rawSuffix) { languageCode = String(languageCode.dropLast(rawSuffix.count)) @@ -1866,10 +1876,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } else if languageCode == "pt-br" { languageCode = "pt" } + let translateToLanguageSG = languageCode + var translateToLanguage: String? + if let translationState, (isPremium || true) && translationState.isEnabled { translateToLanguage = languageCode } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), starGifts: starGifts) + let associatedData = extractAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), starGifts: starGifts) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 292b53b3d96..0816caf6cf6 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import TelegramCore @@ -252,7 +253,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } else { stickersAreEmoji = stickersAreEmoji || hasForward - if stickersEnabled { + if stickersEnabled, !SGSimpleSettings.shared.forceEmojiTab { accessoryItems.append(.input(isEnabled: true, inputMode: stickersAreEmoji ? .emoji : .stickers)) } else { accessoryItems.append(.input(isEnabled: true, inputMode: .emoji)) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1ca2adc044c..f7d49e51267 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1,3 +1,6 @@ +import SGStrings +import SGSimpleSettings +import PeerInfoUI import Foundation import UIKit import Postbox @@ -481,6 +484,16 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if case .standard(.embedded) = chatPresentationInterfaceState.mode { isEmbeddedMode = true } + // MARK: Swiftgram + var canReveal = false + if !chatPresentationInterfaceState.copyProtectionEnabled { + outer: for message in messages { + if message.canRevealContent(contentSettings: context.currentContentSettings.with { $0 }) { + canReveal = true + break outer + } + } + } if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject, case .hashTagSearch = customChatContents.kind { isEmbeddedMode = true @@ -630,7 +643,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState messageEntities = attribute.entities } if let attribute = attribute as? RestrictedContentMessageAttribute { - restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } } @@ -924,6 +937,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let isPremium = accountPeer?.isPremium ?? false var actions: [ContextMenuItem] = [] + var sgActions: [ContextMenuItem] = [] var isPinnedMessages = false if case .pinnedMessages = chatPresentationInterfaceState.subject { @@ -1132,6 +1146,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) }) }))) + if !SGSimpleSettings.shared.contextShowReply { sgActions.append(actions.removeLast()) } } if data.messageActions.options.contains(.sendScheduledNow) { @@ -1220,7 +1235,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState messageEntities = attribute.entities } if let attribute = attribute as? RestrictedContentMessageAttribute { - restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } } @@ -1337,6 +1352,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) f(.default) }))) + if !SGSimpleSettings.shared.contextShowSaveMedia { sgActions.append(actions.removeLast()) } } } @@ -1382,6 +1398,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } + let showJsonAction: ContextMenuItem = .action(ContextMenuActionItem(text: "JSON", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + showMessageJson(controllerInteraction: controllerInteraction, chatPresentationInterfaceState: chatPresentationInterfaceState, message: message, context: context) + f(.default) + })) + if SGSimpleSettings.shared.contextShowJson { + actions.append(showJsonAction) + } else { + sgActions.append(showJsonAction) + } + var threadId: Int64? var threadMessageCount: Int = 0 if case .peer = chatPresentationInterfaceState.chatLocation, let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .group = channel.info { @@ -1420,6 +1448,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controllerInteraction.openMessageReplies(messages[0].id, true, true) }) }))) + if !SGSimpleSettings.shared.contextShowMessageReplies { sgActions.append(actions.removeLast()) } } let isMigrated: Bool @@ -1497,6 +1526,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState interfaceInteraction.pinMessage(messages[0].id, c) }))) } + if !SGSimpleSettings.shared.contextShowPin { sgActions.append(actions.removeLast()) } } if let activePoll = activePoll, messages[0].forwardInfo == nil { @@ -1669,18 +1699,52 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message]) + interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message], nil) f(.dismissWithoutContent) }))) + if message.id.peerId != context.account.peerId { + let action: ContextMenuItem = .action(ContextMenuActionItem(text: i18n("ContextMenu.SaveToCloud", chatPresentationInterfaceState.strings.baseLanguageCode), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message], "forwardMessagesToCloud") + f(.dismissWithoutContent) + })) + if SGSimpleSettings.shared.contextShowSaveToCloud { + actions.append(action) + } else { + sgActions.append(action) + } + } + let action: ContextMenuItem = .action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.NotificationSettings_Stories_CompactHideName, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message], "forwardMessagesWithNoNames") + f(.dismissWithoutContent) + })) + if SGSimpleSettings.shared.contextShowHideForwardName { + actions.append(action) + } else { + sgActions.append(action) + } } } - if data.messageActions.options.contains(.report) { + if canReveal { + actions.insert(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Username_ActivateAlertShow, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Premium/Stories/Views" /*"Chat/Context Menu/Eye"*/ ), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message], "forwardMessagesToCloudWithNoNamesAndOpen") + f(.dismissWithoutContent) + })), at: 0) + } + + if data.messageActions.options.contains(.report) || context.account.testingEnvironment { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuReport, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.actionSheet.primaryTextColor) }, action: { controller, f in interfaceInteraction.reportMessages(messages, controller) }))) + if !SGSimpleSettings.shared.contextShowReport { sgActions.append(actions.removeLast()) } } else if message.id.peerId.isReplies { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuBlock, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.destructiveActionTextColor) @@ -1689,6 +1753,54 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } + if let peer = chatPresentationInterfaceState.renderedPeer?.peer ?? message.peers[message.id.peerId] { + let hasRestrictPermission: Bool + if let channel = peer as? TelegramChannel { + hasRestrictPermission = channel.hasPermission(.banMembers) + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator: + hasRestrictPermission = true + case let .admin(adminRights, _): + hasRestrictPermission = adminRights.rights.contains(.canBanUsers) + case .member: + hasRestrictPermission = false + } + } else { + hasRestrictPermission = false + } + + if let user = message.author as? TelegramUser { + if (user.id != context.account.peerId) && hasRestrictPermission { + let banDisposables = DisposableDict() + // TODO(swiftgram): Check is user an admin? + let action: ContextMenuItem = .action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuBan, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + let participantSignal: Signal + if peer is TelegramChannel { + participantSignal = context.engine.peers.fetchChannelParticipant(peerId: peer.id, participantId: user.id) + } else if peer is TelegramGroup { + participantSignal = .single(.member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil)) + } else { + participantSignal = .single(nil) + } + banDisposables.set((participantSignal + |> deliverOnMainQueue).start(next: { participant in + controllerInteraction.presentController(channelBannedMemberController(context: context, peerId: peer.id, memberId: message.author!.id, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), forKey: user.id) + f(.dismissWithoutContent) + })) + if SGSimpleSettings.shared.contextShowRestrict { + actions.append(action) + } else { + sgActions.append(action) + } + } + } + } + + var clearCacheAsDelete = false if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, !isMigrated { var views: Int = 0 @@ -1807,9 +1919,86 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } } - + var sgActionsIndex: Int? = nil if !isPinnedMessages, !isReplyThreadHead, data.canSelect { + sgActionsIndex = actions.count var didAddSeparator = false + // MARK: Swiftgram + if let authorId = message.author?.id { + let action: ContextMenuItem = .action(ContextMenuActionItem(text: i18n("ContextMenu.SelectFromUser", chatPresentationInterfaceState.strings.baseLanguageCode), icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/SelectAll"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + let progressSignal = Signal { subscriber in + let overlayController = OverlayStatusController(theme: chatPresentationInterfaceState.theme, type: .loading(cancelled: nil)) + controllerInteraction.presentGlobalOverlayController(overlayController, nil) + return ActionDisposable { [weak overlayController] in + Queue.mainQueue().async() { + overlayController?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.2, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + let _ = (context.account.postbox.transaction { transaction -> [MessageId] in + let limit = 500 + var result: [MessageId] = [] + + let needThreadIdFilter: Bool + let searchThreadId: Int64? + switch chatPresentationInterfaceState.chatLocation { + case let .replyThread(replyThreadMessage): + needThreadIdFilter = true + searchThreadId = replyThreadMessage.threadId + default: + needThreadIdFilter = false + searchThreadId = nil + } + transaction.withAllMessages(peerId: message.id.peerId, reversed: true, { searchMessage in + if result.count >= limit { + return false + } + if searchMessage.author?.id == authorId { + // Only messages from current opened thread + // print("searchMessage.threadId:\(String(describing: searchMessage.threadId)) threadId:\(String(describing: threadId)) message.threadId:\(String(describing:message.threadId)) needThreadIdFilter:\(needThreadIdFilter) searchThreadId:\(String(describing:searchThreadId))") + if needThreadIdFilter && searchMessage.threadId != searchThreadId { + return true + } + // No service messages + if searchMessage.media.contains(where: { $0 is TelegramMediaAction }) { + return true + } + result.append(searchMessage.id) + } + return true + }) + return result + } + |> deliverOnMainQueue) + .start(next: { ids in + interfaceInteraction.beginMessageSelection(ids, { transition in + f(.custom(transition)) + }) + Queue.mainQueue().async { + progressDisposable.dispose() + } + }, completed: { + Queue.mainQueue().async { + progressDisposable.dispose() + } + }) + })) + if SGSimpleSettings.shared.contextShowSelectFromUser { + if !actions.isEmpty && !didAddSeparator { + didAddSeparator = true + actions.append(.separator) + } + actions.append(action) + } else { + sgActions.append(action) + } + } + if !selectAll || messages.count == 1 { if !actions.isEmpty && !didAddSeparator { didAddSeparator = true @@ -1841,6 +2030,40 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + // MARK: Swiftgram + if !sgActions.isEmpty { + if !actions.isEmpty { + if let sgActionsIndex = sgActionsIndex { + actions.insert(.separator, at: sgActionsIndex) + } else { + actions.append(.separator) + } + } + + var popSGItems: (() -> Void)? = nil + sgActions.insert(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { _, _ in + popSGItems?() + })), at: 0) + sgActions.insert(.separator, at: 1) + + let swiftgramSubMenu: ContextMenuItem = .action(ContextMenuActionItem(text: "Swiftgram", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "SwiftgramContextMenu"), color: theme.actionSheet.primaryTextColor) + }, action: { c, f in + popSGItems = { [weak c] in + c?.popItems() + } + c?.pushItems(items: .single(ContextController.Items(content: .list(sgActions)))) + })) + + if let sgActionsIndex = sgActionsIndex { + actions.insert(swiftgramSubMenu, at: sgActionsIndex + 1) + } else { + actions.append(swiftgramSubMenu) + } + } + let canViewStats: Bool if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden { canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, isPremium: isPremium, appConfig: appConfig) @@ -1993,7 +2216,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState messageEntities = attribute.entities } if let attribute = attribute as? RestrictedContentMessageAttribute { - restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? "" + restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) ?? "" } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index c81bd6ea28d..217d0a9d7c9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -19,7 +19,7 @@ func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentationInter return false } -func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { +func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, forceHideChannelButton: Bool = false) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { return (nil, nil) } @@ -281,6 +281,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if chatPresentationInterfaceState.interfaceState.editMessage != nil, channel.hasPermission(.editAllMessages) { displayInputTextPanel = true } else if !channel.hasPermission(.sendSomething) || !isMember { + // MARK: Swiftgram + if isMember && forceHideChannelButton { + return (nil, nil) + } if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) { return (currentPanel, nil) } else { diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 172cc9ef83f..fcaf97fa91c 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -513,9 +513,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } if currentTranslateToLanguageUpdated || messageUpdated, let message = interfaceState.pinnedMessage?.message { - if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage { + if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage || translation.toLang.hasPrefix("\(translateToLanguage ?? "")-") /* MARK: Swiftgram */ { } else if let translateToLanguage { - self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], toLang: translateToLanguage).startStrict()) + self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], toLang: translateToLanguage, viaText: !self.context.isPremium).startStrict()) } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 08a9f1aab03..8ffce66ba3a 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1,3 +1,7 @@ +// MARK: Swiftgram +import TelegramUIPreferences +import SGSimpleSettings + import Foundation import UIKit import Display @@ -607,6 +611,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private let hapticFeedback = HapticFeedback() + // MARK: Swiftgram + private var sendWithReturnKey: Bool + private var sendWithReturnKeyDisposable: Disposable? + var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -869,6 +877,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.slowModeButton.alpha = 0.0 self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce) + self.sendWithReturnKey = SGUISettings.default.sendWithReturnKey super.init() @@ -902,6 +911,25 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.context = context + // MARK: Swiftgram + let sendWithReturnKeySignal = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings]) + |> map { view -> Bool in + let settings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? .default + return settings.sendWithReturnKey + } + |> distinctUntilChanged + + self.sendWithReturnKeyDisposable = (sendWithReturnKeySignal + |> deliverOnMainQueue).startStrict(next: { [weak self] value in + if let strongSelf = self { + strongSelf.sendWithReturnKey = value + if let textInputNode = strongSelf.textInputNode { + textInputNode.textView.returnKeyType = strongSelf.sendWithReturnKey ? .send : .default + textInputNode.textView.reloadInputViews() + } + } + }) + self.addSubnode(self.clippingNode) self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in @@ -955,6 +983,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) + // MARK: Swiftgram + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.attachmentButtonLongPressed(_:))) + longPressGesture.minimumPressDuration = 1.0 + self.attachmentButton.view.addGestureRecognizer(longPressGesture) self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) @@ -1121,6 +1153,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch deinit { self.statusDisposable.dispose() + self.sendWithReturnKeyDisposable?.dispose() self.startingBotDisposable.dispose() self.tooltipController?.dismiss() self.currentEmojiSuggestion?.disposable.dispose() @@ -1174,6 +1207,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.textInputContainer.addSubnode(textInputNode) textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled + textInputNode.textView.returnKeyType = self.sendWithReturnKey ? .send : .default self.textInputNode = textInputNode var accessoryButtonsWidth: CGFloat = 0.0 @@ -1621,7 +1655,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState - if let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil { + if !SGSimpleSettings.shared.disableSendAsButton, let sendAsPeers = interfaceState.sendAsPeers, !sendAsPeers.isEmpty && interfaceState.editMessageState == nil { hasMenuButton = true menuButtonExpanded = false isSendAsButton = true @@ -3816,7 +3850,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) { + if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton || SGSimpleSettings.shared.hideRecordingButton) { hideMicButton = true if self.actionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero { @@ -4363,6 +4397,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } self.updateActivity() + + // MARK: Swiftgram + if self.sendWithReturnKey && text == "\n" { + self.sendButtonPressed() + return false + } + var cleanText = text let removeSequences: [String] = ["\u{202d}", "\u{202c}"] for sequence in removeSequences { @@ -4554,6 +4595,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.displayAttachmentMenu() } + // MARK: Swiftgram + @objc func attachmentButtonLongPressed(_ gesture: UILongPressGestureRecognizer) { + guard gesture.state == .began else { return } + guard let _ = self.interfaceInteraction?.chatController() as? ChatControllerImpl else { + return + } + // controller.openStickerEditor() + } + @objc func searchLayoutClearButtonPressed() { if let interfaceInteraction = self.interfaceInteraction { interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index 32ca974cf6f..5992ebecfd2 100644 --- a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift @@ -154,13 +154,14 @@ final class ChatTranslationPanelNode: ASDisplayNode { let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize) - if interfaceState.isPremium { + // MARK: Swiftgram + // if interfaceState.isPremium { self.moreButton.isHidden = false self.closeButton.isHidden = true - } else { + /* } else { self.moreButton.isHidden = true self.closeButton.isHidden = false - } + }*/ let buttonPadding: CGFloat = 10.0 let buttonSpacing: CGFloat = 10.0 @@ -185,9 +186,9 @@ final class ChatTranslationPanelNode: ASDisplayNode { guard let translationState = self.chatInterfaceState?.translationState else { return } - + // MARK: Swiftgram let isPremium = self.chatInterfaceState?.isPremium ?? false - if isPremium { + if isPremium || true { self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated) } else if !translationState.isEnabled { let context = self.context diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a7712b527bb..568e23f754f 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -1,4 +1,5 @@ import Foundation +import SGSimpleSettings import Display import AsyncDisplayKit import Postbox diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 919032eb4d8..6158bbf1e66 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1,3 +1,9 @@ +import SGLogging +import SGAPIWebSettings +import SGConfig +import SGSettingsUI +import SGDebugUI +import SFSafariViewControllerPlus import Foundation import Display import SafariServices @@ -976,7 +982,28 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } } else { - if parsedUrl.host == "importStickers" { + if parsedUrl.host == "sg" { + if let path = parsedUrl.pathComponents.last { + switch path { + case "debug": + if let debugController = context.sharedContext.makeDebugSettingsController(context: context) { + navigationController?.pushViewController(debugController) + return + } + case "sgdebug", "sg_debug": + navigationController?.pushViewController(sgDebugController(context: context)) + return + case "settings": + navigationController?.pushViewController(sgSettingsController(context: context)) + return + case "ios_settings": + context.sharedContext.applicationBindings.openSettings() + return + default: + break + } + } + } else if parsedUrl.host == "importStickers" { handleResolvedUrl(.importStickers) } else if parsedUrl.host == "settings" { if let path = parsedUrl.pathComponents.last { @@ -1093,15 +1120,24 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur break } } + // MARK: Swiftgram + if settings.defaultWebBrowser == "inApp" { isExceptedDomain = false} if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { if let window = navigationController?.view.window, !isExceptedDomain { - let controller = SFSafariViewController(url: parsedUrl) + // MARK: Swiftgram + let controller = SFSafariViewControllerPlusDidFinish(url: parsedUrl) controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + if parsedUrl.host?.lowercased() == SG_API_WEBAPP_URL_PARSED.host?.lowercased() { + controller.onDidFinish = { + SGLogger.shared.log("SafariController", "Closed webapp") + updateSGWebSettingsInteractivelly(context: context) + } + } window.rootViewController?.present(controller, animated: true) } else { context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 291257a38b1..73c0a76b24f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import Display @@ -73,6 +74,8 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh public final class TelegramRootController: NavigationController, TelegramRootControllerInterface { private let context: AccountContext + private var showTabNames: Bool + public var rootTabController: TabBarController? public var contactsController: ContactsController? @@ -98,9 +101,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon public var minimizedContainerUpdated: (MinimizedContainer?) -> Void = { _ in } - public init(context: AccountContext) { + public init(showTabNames: Bool, context: AccountContext) { self.context = context + self.showTabNames = showTabNames + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } super.init(mode: .automaticMasterDetail, theme: NavigationControllerTheme(presentationTheme: self.presentationData.theme)) @@ -186,8 +191,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon super.containerLayoutUpdated(layout, transition: transition) } - public func addRootControllers(showCallsTab: Bool) { - let tabBarController = TabBarControllerImpl(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), theme: TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) + public func addRootControllers(hidePhoneInSettings: Bool, showContactsTab: Bool, showCallsTab: Bool) { + let tabBarController = TabBarControllerImpl(showTabNames: self.showTabNames, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), theme: TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) tabBarController.navigationPresentation = .master let chatListController = self.context.sharedContext.makeChatListController(context: self.context, location: .chatList(groupId: .root), controlsHistoryPreload: true, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild) if let sharedContext = self.context.sharedContext as? SharedAccountContextImpl { @@ -201,7 +206,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon contactsController.switchToChatsController = { [weak self] in self?.openChatsController(activateSearch: false) } - controllers.append(contactsController) + // MARK: Swiftgram + if showContactsTab { + controllers.append(contactsController) + } if showCallsTab { controllers.append(callListController) @@ -217,7 +225,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon sharedContext.switchingData = (nil, nil, nil) } - let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], isSettings: true) + let accountSettingsController = PeerInfoScreenImpl(hidePhoneInSettings: hidePhoneInSettings, context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], isSettings: true) accountSettingsController.tabBarItemDebugTapAction = { [weak self] in guard let strongSelf = self else { return @@ -237,12 +245,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon self.pushViewController(tabBarController, animated: false) } - public func updateRootControllers(showCallsTab: Bool) { + public func updateRootControllers(showContactsTab: Bool, showCallsTab: Bool) { guard let rootTabController = self.rootTabController as? TabBarControllerImpl else { return } var controllers: [ViewController] = [] - controllers.append(self.contactsController!) + if showContactsTab { + controllers.append(self.contactsController!) + } if showCallsTab { controllers.append(self.callListController!) } @@ -642,7 +652,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon defer { TempBox.shared.dispose(tempFile) } - if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { + if let imageData = compressImageToJPEG(image, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) { media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers) } case let .video(content, firstFrameImage, values, duration, dimensions): @@ -665,7 +675,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon defer { TempBox.shared.dispose(tempFile) } - let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } + let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) } let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in let file = TempBox.shared.tempFile(fileName: "image.jpg") if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index 51b7b85aac8..aa94c8b521e 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import TelegramCore @@ -169,7 +170,8 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me defer { TempBox.shared.dispose(tempFile) } - if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: 0.7, tempFilePath: tempFile.path) { + // MARK: Swiftgram + if let fullImage = UIImage(contentsOfFile: data.path), let smallestImage = generateScaledImage(image: fullImage, size: smallestSize, scale: 1.0), let smallestData = compressImageToJPEG(smallestImage, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) { var representations = image.representations let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) diff --git a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift index 543664e00b7..13eb3c254d3 100644 --- a/submodules/TelegramUIPreferences/Sources/CallListSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/CallListSettings.swift @@ -3,11 +3,12 @@ import TelegramCore import SwiftSignalKit public struct CallListSettings: Codable, Equatable { + public var showContactsTab: Bool public var _showTab: Bool? public var defaultShowTab: Bool? public static var defaultSettings: CallListSettings { - return CallListSettings(showTab: false) + return CallListSettings(showContactsTab: true, showTab: false) } public var showTab: Bool { @@ -24,18 +25,21 @@ public struct CallListSettings: Codable, Equatable { } } - public init(showTab: Bool) { + public init(showContactsTab: Bool, showTab: Bool) { + self.showContactsTab = showContactsTab self._showTab = showTab } - public init(showTab: Bool?, defaultShowTab: Bool?) { + public init(showContactsTab: Bool, showTab: Bool?, defaultShowTab: Bool?) { self._showTab = showTab + self.showContactsTab = showContactsTab self.defaultShowTab = defaultShowTab } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - + + self.showContactsTab = (try container.decode(Int32.self, forKey: "showContactsTab")) != 0 if let alternativeDefaultValue = try container.decodeIfPresent(Int32.self, forKey: "defaultShowTab") { self.defaultShowTab = alternativeDefaultValue != 0 } @@ -46,7 +50,9 @@ public struct CallListSettings: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - + + try container.encode((self.showContactsTab ? 1 : 0) as Int32, forKey: "showContactsTab") + if let defaultShowTab = self.defaultShowTab { try container.encode((defaultShowTab ? 1 : 0) as Int32, forKey: "defaultShowTab") } else { @@ -60,11 +66,15 @@ public struct CallListSettings: Codable, Equatable { } public static func ==(lhs: CallListSettings, rhs: CallListSettings) -> Bool { - return lhs._showTab == rhs._showTab && lhs.defaultShowTab == rhs.defaultShowTab + return lhs.showContactsTab == rhs.showContactsTab && lhs._showTab == rhs._showTab && lhs.defaultShowTab == rhs.defaultShowTab } public func withUpdatedShowTab(_ showTab: Bool) -> CallListSettings { - return CallListSettings(showTab: showTab, defaultShowTab: self.defaultShowTab) + return CallListSettings(showContactsTab: self.showContactsTab, showTab: showTab, defaultShowTab: self.defaultShowTab) + } + + public func withUpdatedShowContactsTab(_ showContactsTab: Bool) -> CallListSettings { + return CallListSettings(showContactsTab: showContactsTab, showTab: self.showTab, defaultShowTab: self.defaultShowTab) } } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index ebefd6a4f95..cf1b43a54d3 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -3,6 +3,7 @@ import TelegramCore import Postbox private enum ApplicationSpecificPreferencesKeyValues: Int32 { + case SGUISettings = 900 case voipDerivedState = 16 case chatArchiveSettings = 17 case chatListFilterSettings = 18 @@ -11,6 +12,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { } public struct ApplicationSpecificPreferencesKeys { + public static let SGUISettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.SGUISettings.rawValue) public static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue) public static let chatArchiveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatArchiveSettings.rawValue) public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue) diff --git a/submodules/TelegramUIPreferences/Sources/Swiftgram/SGUISettings.swift b/submodules/TelegramUIPreferences/Sources/Swiftgram/SGUISettings.swift new file mode 100644 index 00000000000..c6a0054f40e --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/Swiftgram/SGUISettings.swift @@ -0,0 +1,51 @@ +import Foundation +import SwiftSignalKit +import TelegramCore + +public struct SGUISettings: Equatable, Codable { + public var hideStories: Bool + public var showProfileId: Bool + public var warnOnStoriesOpen: Bool + public var sendWithReturnKey: Bool + + public static var `default`: SGUISettings { + return SGUISettings(hideStories: false, showProfileId: true, warnOnStoriesOpen: false, sendWithReturnKey: false) + } + + public init(hideStories: Bool, showProfileId: Bool, warnOnStoriesOpen: Bool, sendWithReturnKey: Bool) { + self.hideStories = hideStories + self.showProfileId = showProfileId + self.warnOnStoriesOpen = warnOnStoriesOpen + self.sendWithReturnKey = sendWithReturnKey + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.hideStories = (try container.decode(Int32.self, forKey: "hideStories")) != 0 + self.showProfileId = (try container.decode(Int32.self, forKey: "showProfileId")) != 0 + self.warnOnStoriesOpen = (try container.decode(Int32.self, forKey: "warnOnStoriesOpen")) != 0 + self.sendWithReturnKey = (try container.decode(Int32.self, forKey: "sendWithReturnKey")) != 0 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode((self.hideStories ? 1 : 0) as Int32, forKey: "hideStories") + try container.encode((self.showProfileId ? 1 : 0) as Int32, forKey: "showProfileId") + try container.encode((self.warnOnStoriesOpen ? 1 : 0) as Int32, forKey: "warnOnStoriesOpen") + try container.encode((self.sendWithReturnKey ? 1 : 0) as Int32, forKey: "sendWithReturnKey") + } +} + +public func updateSGUISettings(engine: TelegramEngine, _ f: @escaping (SGUISettings) -> SGUISettings) -> Signal { + return engine.preferences.update(id: ApplicationSpecificPreferencesKeys.SGUISettings, { entry in + let currentSettings: SGUISettings + if let entry = entry?.get(SGUISettings.self) { + currentSettings = entry + } else { + currentSettings = .default + } + return SharedPreferencesEntry(f(currentSettings)) + }) +} diff --git a/submodules/TranslateUI/BUILD b/submodules/TranslateUI/BUILD index 6de2b55b35a..3ab31455e4e 100644 --- a/submodules/TranslateUI/BUILD +++ b/submodules/TranslateUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//submodules/TextFormat:TextFormat" +] + swift_library( name = "TranslateUI", module_name = "TranslateUI", @@ -9,7 +13,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 1d0d9e70204..883b7aa26eb 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -1,3 +1,4 @@ +import TextFormat import Foundation import NaturalLanguage import SwiftSignalKit @@ -48,6 +49,14 @@ public struct ChatTranslationState: Codable { try container.encode(self.isEnabled, forKey: .isEnabled) } + public func withFromLang(_ fromLang: String) -> ChatTranslationState { + return ChatTranslationState( + baseLang: self.baseLang, + fromLang: fromLang, + toLang: self.toLang, + isEnabled: self.isEnabled + ) + } public func withToLang(_ toLang: String?) -> ChatTranslationState { return ChatTranslationState( baseLang: self.baseLang, @@ -109,8 +118,9 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer @available(iOS 12.0, *) private let languageRecognizer = NLLanguageRecognizer() -public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal { +public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String, viaText: Bool = false, forQuickTranslate: Bool = false) -> Signal { return context.account.postbox.transaction { transaction -> Signal in + var messageDictToTranslate: [EngineMessage.Id: String] = [:] var messageIdsToTranslate: [EngineMessage.Id] = [] var messageIdsSet = Set() for messageId in messageIds { @@ -122,11 +132,13 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess if !messageIdsSet.contains(replyMessage.id) { messageIdsToTranslate.append(replyMessage.id) messageIdsSet.insert(replyMessage.id) + messageDictToTranslate[replyMessage.id] = replyMessage.text } } } } - guard message.author?.id != context.account.peerId else { + // MARK: Swiftgram + guard forQuickTranslate || message.author?.id != context.account.peerId else { continue } if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == toLang { @@ -137,8 +149,10 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess if !messageIdsSet.contains(messageId) { messageIdsToTranslate.append(messageId) messageIdsSet.insert(messageId) + messageDictToTranslate[messageId] = message.text } - } else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) { + // TODO(swiftgram): Translate polls + } else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }), !viaText { if !messageIdsSet.contains(messageId) { messageIdsToTranslate.append(messageId) messageIdsSet.insert(messageId) @@ -151,14 +165,24 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess } } } + if viaText { + return context.engine.messages.translateMessagesViaText(messagesDict: messageDictToTranslate, toLang: toLang, generateEntitiesFunction: { text in + generateTextEntities(text, enabledTypes: .all) + }) + |> `catch` { _ -> Signal in + return .complete() + } + } else { + if forQuickTranslate && messageIdsToTranslate.isEmpty { return .complete() } // Otherwise Telegram's API will return .never() return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, toLang: toLang) |> `catch` { _ -> Signal in return .complete() } + } } |> switchToLatest } -public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) -> Signal { +public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, forcePredict: Bool = false) -> Signal { if peerId.id == EnginePeer.Id.Id._internalFromInt64Value(777000) { return .single(nil) } @@ -175,7 +199,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) return context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> mapToSignal { sharedData in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings - if !settings.translateChats { + if !settings.translateChats && !forcePredict { return .single(nil) } @@ -192,7 +216,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) return cachedChatTranslationState(engine: context.engine, peerId: peerId) |> mapToSignal { cached in if let cached, cached.baseLang == baseLang { - if !dontTranslateLanguages.contains(cached.fromLang) { + if !dontTranslateLanguages.contains(cached.fromLang) || forcePredict { return .single(cached) } else { return .single(nil) @@ -289,7 +313,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) } let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() - if !dontTranslateLanguages.contains(fromLang) { + if !dontTranslateLanguages.contains(fromLang) || forcePredict { return state } else { return nil diff --git a/submodules/TranslateUI/Sources/LanguageSelectionController.swift b/submodules/TranslateUI/Sources/LanguageSelectionController.swift index a9d2e4ea86f..07c488f105f 100644 --- a/submodules/TranslateUI/Sources/LanguageSelectionController.swift +++ b/submodules/TranslateUI/Sources/LanguageSelectionController.swift @@ -90,7 +90,7 @@ private struct LanguageSelectionControllerState: Equatable { var toLanguage: String } -public func languageSelectionController(context: AccountContext, forceTheme: PresentationTheme? = nil, fromLanguage: String, toLanguage: String, completion: @escaping (String, String) -> Void) -> ViewController { +public func languageSelectionController(translateOutgoingMessage: Bool = false, context: AccountContext, forceTheme: PresentationTheme? = nil, fromLanguage: String, toLanguage: String, completion: @escaping (String, String) -> Void) -> ViewController { let statePromise = ValuePromise(LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage), ignoreRepeated: true) let stateValue = Atomic(value: LanguageSelectionControllerState(section: .translation, fromLanguage: fromLanguage, toLanguage: toLanguage)) let updateState: ((LanguageSelectionControllerState) -> LanguageSelectionControllerState) -> Void = { f in @@ -113,6 +113,7 @@ public func languageSelectionController(context: AccountContext, forceTheme: Pre case .translation: updated.toLanguage = code } + if translateOutgoingMessage { completion(updated.fromLanguage, updated.toLanguage); dismissImpl?() } return updated } }) @@ -153,7 +154,7 @@ public func languageSelectionController(context: AccountContext, forceTheme: Pre if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: translateOutgoingMessage ? .sectionControl([presentationData.strings.Translate_Languages_Translation], 0) : .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { completion(state.fromLanguage, state.toLanguage) dismissImpl?() }), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 751c1bdeb67..5db1c31f407 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -338,7 +338,8 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, t let (items, focusItem) = galleryItems(account: context.account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false)! + let currentAppConfiguration = context.currentAppConfiguration.with { $0 } + let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false, canShowTelescope: currentAppConfiguration.sgWebSettings.global.canShowTelescope, canSendTelescope: currentAppConfiguration.sgWebSettings.user.canSendTelescope)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 19b80d186ed..9fe8b1cd983 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -1,5 +1,11 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGAPIWebSettings:SGAPIWebSettings", + "//Swiftgram/SGConfig:SGConfig", + "//Swiftgram/SGLogging:SGLogging" +] + swift_library( name = "WebUI", module_name = "WebUI", @@ -9,7 +15,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 9ee319ed4f0..845beefcce8 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1,3 +1,6 @@ +import SGConfig +import SGAPIWebSettings +import SGLogging import Foundation import UIKit @preconcurrency import WebKit @@ -172,7 +175,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private var validLayout: (ContainerViewLayout, CGFloat)? - init(context: AccountContext, controller: WebAppController) { + init(userScripts: [WKUserScript] = [], context: AccountContext, controller: WebAppController) { self.context = context self.controller = controller self.presentationData = controller.presentationData @@ -189,7 +192,16 @@ public final class WebAppController: ViewController, AttachmentContainable { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } - let webView = WebAppWebView(account: context.account) + // MARK: Swiftgram + var userScripts: [WKUserScript] = [] + let globalSGConfig = context.currentAppConfiguration.with({ $0 }).sgWebSettings.global + let botIdInt = controller.botId.id._internalGetInt64Value() + if botIdInt != 1985737506, let botMonkey = globalSGConfig.botMonkeys.first(where: { $0.botId == botIdInt}) { + if !botMonkey.src.isEmpty { + userScripts.append(WKUserScript(source: botMonkey.src, injectionTime: .atDocumentStart, forMainFrameOnly: false)) + } + } + let webView = WebAppWebView(userScripts: userScripts, account: context.account) webView.alpha = 0.0 webView.navigationDelegate = self webView.uiDelegate = self @@ -927,7 +939,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true) } case "web_app_trigger_haptic_feedback": - if let json = json, let type = json["type"] as? String { + if let json = json, let type = json["type"] as? String, !(self.webView?.monkeyClickerActive ?? false) { switch type { case "impact": if let impactType = json["impact_style"] as? String { @@ -1872,6 +1884,7 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? + private var viewWillDisappearCalled = false private var hasSettings = false public var openUrl: (String, Bool, @escaping () -> Void) -> Void = { _, _, _ in } @@ -2132,6 +2145,19 @@ public final class WebAppController: ViewController, AttachmentContainable { }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) }) }))) + + // MARK: Swiftgram + let globalSGConfig = context.currentAppConfiguration.with({ $0 }).sgWebSettings.global + let botIdInt = botId.id._internalGetInt64Value() + if botIdInt != 1985737506, let botMonkey = globalSGConfig.botMonkeys.first(where: { $0.botId == botIdInt}) { + let itemText = (self?.controllerNode.webView?.monkeyClickerActive ?? false) ? "Disable Clicker" : "Enable Clicker" + items.append(.action(ContextMenuActionItem(text: itemText, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + self?.controllerNode.webView?.toggleClicker(enableJS: botMonkey.enable, disableJS: botMonkey.disable) + }))) + } items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_PrivacyPolicy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Privacy"), color: theme.contextMenu.primaryColor) @@ -2219,6 +2245,24 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controllerNode.setupWebView() } + + // MARK: Swiftgram + override final public func viewWillDisappear(_ animated: Bool) { + if !self.viewWillDisappearCalled { + self.viewWillDisappearCalled = true + self.updateSGWebSettingsIfNeeded() + } + super.viewWillDisappear(animated) + } + + private func updateSGWebSettingsIfNeeded() { + if let url = self.url, let parsedUrl = URL(string: url), parsedUrl.host?.lowercased() == SG_API_WEBAPP_URL_PARSED.host?.lowercased() { + SGLogger.shared.log("WebApp", "Closed webapp") + updateSGWebSettingsInteractivelly(context: self.context) + } + } + + public func requestDismiss(completion: @escaping () -> Void) { if self.controllerNode.needDismissConfirmation { let actionSheet = ActionSheetController(presentationData: self.presentationData) diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index ce602c1b187..83e0671c7fd 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -104,7 +104,7 @@ final class WebAppWebView: WKWebView { return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) } - init(account: Account) { + init(userScripts: [WKUserScript] = [], account: Account) { let configuration = WKWebViewConfiguration() if #available(iOS 17.0, *) { @@ -146,6 +146,10 @@ final class WebAppWebView: WKWebView { let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) contentController.addUserScript(videoScript) + for userScript in userScripts { + contentController.addUserScript(userScript) + } + configuration.userContentController = contentController configuration.allowsInlineMediaPlayback = true @@ -265,6 +269,9 @@ final class WebAppWebView: WKWebView { }) } + // MARK: Swiftgram + public private(set) var monkeyClickerActive = false + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) self.lastTouchTimestamp = CACurrentMediaTime() @@ -279,3 +286,16 @@ final class WebAppWebView: WKWebView { return nil } } + +// MARK: Swiftgram +extension WebAppWebView { + + public func toggleClicker(enableJS: String, disableJS: String) { + if self.monkeyClickerActive { + self.evaluateJavaScript(disableJS, completionHandler: nil) + } else { + self.evaluateJavaScript(enableJS, completionHandler: nil) + } + self.monkeyClickerActive = !self.monkeyClickerActive + } +} diff --git a/versions.json b/versions.json index da4ab2f6e5b..3556a03d85c 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.2", + "app": "11.2.1", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0"