diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7259ebd7e7..f504b663f7 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 4a8af5793f..6942b9417e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +submodules/**/.build/* +swiftgram-scripts +Swiftgram/Playground/custom_bazel_path.bzl +Swiftgram/Playground/codesigning +buildServer.json + fastlane/README.md fastlane/report.xml fastlane/test_output/* diff --git a/.gitmodules b/.gitmodules index c44a0bbf39..830c2d455e 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 @@ -13,7 +12,7 @@ url=https://github.com/bazelbuild/rules_swift.git url = https://github.com/bazelbuild/apple_support.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/.vscode/settings.json b/.vscode/settings.json index 2bb86d2b21..37f77ed4f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "sweetpad.build.xcodeWorkspacePath": "Telegram/Telegram.xcodeproj/project.xcworkspace", + "sweetpad.build.xcodeWorkspacePath": "Telegram/Swiftgram.xcodeproj/project.xcworkspace", "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", "lldb.launch.expressions": "native", "search.followSymlinks": false, diff --git a/README.md b/README.md index 79f325aa13..1f754271a8 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 0000000000..0924cf28e8 --- /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 0000000000..c2ef2cb59e --- /dev/null +++ b/Swiftgram/AppleStyleFolders/Sources/File.swift @@ -0,0 +1,1074 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import SGSimpleSettings +import AccountContext +import TextNodeWithEntities + +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 context: AccountContext + private let pressed: () -> Void + private let requestedDeletion: () -> Void + + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let extractedBackgroundNode: ASImageNode + private let titleNode: ImmediateTextNodeWithEntities + private let shortTitleNode: ImmediateTextNodeWithEntities + 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? + private var currentTitle: (ChatFolderTitle, ChatFolderTitle)? + + init(context: AccountContext, pressed: @escaping () -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture) -> Void) { + self.context = context + 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 = ImmediateTextNodeWithEntities() + self.titleNode.displaysAsynchronously = false + self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.titleNode.resetEmojiToFirstFrameAutomatically = true + + self.shortTitleNode = ImmediateTextNodeWithEntities() + self.shortTitleNode.displaysAsynchronously = false + self.shortTitleNode.alpha = 0.0 + self.shortTitleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0) + self.shortTitleNode.resetEmojiToFirstFrameAutomatically = true + + 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: ChatFolderTitle, shortTitle: ChatFolderTitle, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, isSelected: Bool, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + + var themeUpdated = false + 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) + + themeUpdated = true + } + // MARK: Swiftgram + var titleUpdated = false + if self.currentTitle?.0 != title || self.currentTitle?.1 != shortTitle { + self.currentTitle = (title, shortTitle) + + titleUpdated = true + } + // + + 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) + // MARK: Swiftgram + let titleArguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + placeholderColor: presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + + self.titleNode.arguments = titleArguments + self.shortTitleNode.arguments = titleArguments + + self.titleNode.visibility = title.enableAnimations + self.shortTitleNode.visibility = title.enableAnimations + + if themeUpdated || titleUpdated { + self.titleNode.attributedText = title.attributedString(font: Font.bold(17.0), textColor: isSelected ? presentationData.theme.contextMenu.badgeForegroundColor : presentationData.theme.list.itemSecondaryTextColor) + self.shortTitleNode.attributedText = shortTitle.attributedString(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 context: AccountContext + 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 + } + } + } + } + + init(context: AccountContext) { + self.context = context + 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(context: self.context, 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(context: self.context, 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 0000000000..15c650e14a --- /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 0000000000..23d5c46b4c --- /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 0000000000..e69de29bb2 diff --git a/Swiftgram/FLEX/FLEX.BUILD b/Swiftgram/FLEX/FLEX.BUILD new file mode 100644 index 0000000000..52e69f6916 --- /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 0000000000..842cb77a79 --- /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 0000000000..f860378633 --- /dev/null +++ b/Swiftgram/Playground/BUILD @@ -0,0 +1,87 @@ +load("@bazel_skylib//rules:common_settings.bzl", + "bool_flag", +) +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_targets", + "xcodeproj", +) +load( + "@build_configuration//:variables.bzl", "telegram_bazel_path" +) + +bool_flag( + name = "disableProvisioningProfiles", + build_setting_default = False, + visibility = ["//visibility:public"], +) + +config_setting( + name = "disableProvisioningProfilesSetting", + flag_values = { + ":disableProvisioningProfiles": "True", + }, +) + +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", + "//submodules/LegacyUI:LegacyUI", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//Swiftgram/SGSwiftUI:SGSwiftUI", + ], + data = [ + "//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data", + ], + visibility = ["//visibility:public"], +) + +ios_application( + name = "Playground", + bundle_id = "app.swiftgram.ios.Playground", + families = [ + "iphone", + "ipad", + ], + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "codesigning/Playground.mobileprovision", + }), + infoplists = ["Resources/Info.plist"], + minimum_os_version = "14.0", + visibility = ["//visibility:public"], + strings = [ + "//Telegram:AppStringResources", + ], + launch_storyboard = "Resources/LaunchScreen.storyboard", + deps = [":PlaygroundMain", ":PlaygroundLib"], +) + +xcodeproj( + bazel_path = telegram_bazel_path, + name = "Playground_xcodeproj", + build_mode = "bazel", + project_name = "Playground", + tags = ["manual"], + top_level_targets = top_level_targets( + labels = [ + ":Playground", + ], + target_environments = ["device", "simulator"], + ), +) \ No newline at end of file diff --git a/Swiftgram/Playground/README.md b/Swiftgram/Playground/README.md new file mode 100644 index 0000000000..221a308b19 --- /dev/null +++ b/Swiftgram/Playground/README.md @@ -0,0 +1,25 @@ +# Swiftgram Playground + +Small app to quickly iterate on components testing without building an entire messenger. + +## (Optional) Setup Codesigning + +Create simple `codesigning/Playground.mobileprovision`. It is only required for non-simulator builds and can be skipped with `--disableProvisioningProfiles`. + +## Generate Xcode project + +Same as main project described in [../../Readme.md](../../Readme.md), but with `--target="Swiftgram/Playground"` parameter. + +## 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 0000000000..95fdf06b7d --- /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 0000000000..865e9329f3 --- /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 0000000000..69404da227 --- /dev/null +++ b/Swiftgram/Playground/Sources/AppDelegate.swift @@ -0,0 +1,82 @@ +import UIKit +import SwiftUI +import AsyncDisplayKit +import Display +import LegacyUI + +let SHOW_SAFE_AREA = false + +@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 + + let rootViewController = mySwiftUIViewController(0) + + if SHOW_SAFE_AREA { + // Add insets visualization + rootViewController.view.layoutMargins = .zero + rootViewController.view.subviews.forEach { $0.removeFromSuperview() } + + let topInsetView = UIView() + let leftInsetView = UIView() + let rightInsetView = UIView() + let bottomInsetView = UIView() + + [topInsetView, leftInsetView, rightInsetView, bottomInsetView].forEach { + $0.backgroundColor = .systemRed + $0.alpha = 0.3 + rootViewController.view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + topInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + topInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + topInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + topInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.topAnchor), + + leftInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + leftInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + leftInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + leftInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.leadingAnchor), + + rightInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + rightInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + rightInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + rightInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.trailingAnchor), + + bottomInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + bottomInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + bottomInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + bottomInsetView.topAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + navigationController.setViewControllers([rootViewController], 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 0000000000..28b7549d45 --- /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 0000000000..12e8255877 --- /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 0000000000..982fcbf479 --- /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 0000000000..b05d793346 --- /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 0000000000..139230a38a --- /dev/null +++ b/Swiftgram/Playground/Sources/SwiftUIViewController.swift @@ -0,0 +1,85 @@ +import AsyncDisplayKit +import Display +import Foundation +import LegacyUI +import SGSwiftUI +import SwiftUI +import TelegramPresentationData +import UIKit + +struct MySwiftUIView: View { + weak var wrapperController: LegacyController? + + var num: Int64 + + var body: some View { + ScrollView { + Text("Hello, World!") + .font(.title) + .foregroundColor(.black) + + Spacer(minLength: 0) + + Button("Push") { + self.wrapperController?.push(mySwiftUIViewController(num + 1)) + }.buttonStyle(AppleButtonStyle()) + Spacer() + Button("Modal") { + self.wrapperController?.present( + mySwiftUIViewController(num + 1), + in: .window(.root), + with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet) + ) + }.buttonStyle(AppleButtonStyle()) + Spacer() + if num > 0 { + Button("Dismiss") { + self.wrapperController?.dismiss() + }.buttonStyle(AppleButtonStyle()) + Spacer() + } + ForEach(1..<20, id: \.self) { i in + Button("TAP: \(i)") { + print("Tapped \(i)") + }.buttonStyle(AppleButtonStyle()) + } + + } + .background(Color.green) + } +} + +struct AppleButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(minWidth: 0, maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + .opacity(configuration.isPressed ? 0.9 : 1) + } +} + +public func mySwiftUIViewController(_ num: Int64) -> ViewController { + let legacyController = LegacySwiftUIController( + presentation: .modal(animateIn: true), + theme: defaultPresentationTheme, + strings: defaultPresentationStrings + ) + legacyController.statusBar.statusBarStyle = defaultPresentationTheme.rootController + .statusBarStyle.style + legacyController.title = "Controller: \(num)" + + let swiftUIView = SGSwiftUIView( + navigationBarHeight: legacyController.navigationBarHeightModel, + containerViewLayout: legacyController.containerViewLayoutModel, + content: { MySwiftUIView(wrapperController: legacyController, num: num) } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) + + return legacyController +} diff --git a/Swiftgram/Playground/Sources/main.m b/Swiftgram/Playground/Sources/main.m new file mode 100644 index 0000000000..a63f787dda --- /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 0000000000..cc2e135174 --- /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(), cache_host=None) + # 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 0000000000..feefa4f941 --- /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 0000000000..72a719f0b1 --- /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 0000000000..1df3ddbaa3 --- /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 0000000000..1a7634e2c8 --- /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 0000000000..9a85f8093c --- /dev/null +++ b/Swiftgram/SGAPI/Sources/SGAPI.swift @@ -0,0 +1,188 @@ +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() + } + } + } +} + + +public func postSGReceipt(token: String, deviceToken: String, encodedReceiptData: Data) -> Signal { + return Signal { subscriber in + + let url = URL(string: buildApiUrl("validate"))! + 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.httpMethod = "POST" + request.httpBody = encodedReceiptData + + 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("Error posting Receipt: \(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 posting Receipt: \(String(describing: error))")) + }) + + return ActionDisposable { + if !completed.with({ $0 }) { + dataSignal.dispose() + } + } + } +} diff --git a/Swiftgram/SGAPIToken/BUILD b/Swiftgram/SGAPIToken/BUILD new file mode 100644 index 0000000000..9b507e1c2b --- /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 0000000000..209cbb0471 --- /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, referrer: nil) + |> 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 0000000000..9964398d27 --- /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 0000000000..bfb022afaa --- /dev/null +++ b/Swiftgram/SGAPIWebSettings/Sources/File.swift @@ -0,0 +1,50 @@ +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 + SGSimpleSettings.shared.duckyAppIconAvailable = webSettings.global.duckyAppIconAvailable + 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 0000000000..a27377792c --- /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 0000000000..f94edc1c68 --- /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/SGAppGroupIdentifier/BUILD b/Swiftgram/SGAppGroupIdentifier/BUILD new file mode 100644 index 0000000000..cc3e13985c --- /dev/null +++ b/Swiftgram/SGAppGroupIdentifier/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAppGroupIdentifier", + module_name = "SGAppGroupIdentifier", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGAppGroupIdentifier/Sources/SGAppGroupIdentifier.swift b/Swiftgram/SGAppGroupIdentifier/Sources/SGAppGroupIdentifier.swift new file mode 100644 index 0000000000..cf17a07f94 --- /dev/null +++ b/Swiftgram/SGAppGroupIdentifier/Sources/SGAppGroupIdentifier.swift @@ -0,0 +1,28 @@ +import Foundation + +let fallbackBaseBundleId: String = "app.swiftgram.ios" + +public func sgAppGroupIdentifier() -> String { + let baseBundleId: String + if let bundleId: String = Bundle.main.bundleIdentifier { + if Bundle.main.bundlePath.hasSuffix(".appex") { + if let lastDotRange: Range = bundleId.range(of: ".", options: [.backwards]) { + baseBundleId = String(bundleId[.. 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 0000000000..8679395f70 --- /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 0000000000..b75ba3fd3e --- /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 0000000000..c9e2113bd6 --- /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 0000000000..3cb9b93952 --- /dev/null +++ b/Swiftgram/SGDBReset/Sources/File.swift @@ -0,0 +1,162 @@ +import UIKit +import Foundation +import SGLogging + +private let dbResetKey = "sg_db_reset" +private let dbHardResetKey = "sg_db_hard_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: "Metadata 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: "Metadata 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() +} + +public func sgHardReset(dataPath: String, present: ((UIViewController) -> ())?) { + let startAlert = UIAlertController( + title: "ATTENTION", + message: "Confirm RESET ALL?", + preferredStyle: .alert + ) + + startAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + exit(0) + }) + startAlert.addAction(UIAlertAction(title: "RESET", style: .destructive) { _ in + let ensureAlert = UIAlertController( + title: "⚠️ ATTENTION ⚠️", + message: "ARE YOU SURE you want to make a RESET ALL?", + preferredStyle: .alert + ) + + ensureAlert.addAction(UIAlertAction(title: "Cancel", style: .default) { _ in + exit(0) + }) + ensureAlert.addAction(UIAlertAction(title: "RESET NOW", style: .destructive) { _ in + NSLog("[SG.DBReset] Reset All with system settings") + let alert = UIAlertController( + title: "Reset All.\nPlease wait...", + message: nil, + preferredStyle: .alert + ) + ensureAlert.dismiss(animated: false) { + present?(alert) + } + + do { + let fileManager = FileManager.default + let contents = try fileManager.contentsOfDirectory(atPath: dataPath) + + // Filter directories that match our criteria + let accountDirectories = contents.compactMap { filename in + let fullPath = (dataPath as NSString).appendingPathComponent(filename) + + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory), isDirectory.boolValue { + if filename.hasPrefix("account-") || filename == "accounts-metadata" { + return fullPath + } + } + return nil + } + + NSLog("[SG.DBReset] Found \(accountDirectories.count) account dirs...") + var deletedPostboxCount = 0 + for accountDir in accountDirectories { + let accountName = (accountDir as NSString).lastPathComponent + let postboxPath = (accountDir as NSString).appendingPathComponent("postbox") + + var isPostboxDir: ObjCBool = false + if fileManager.fileExists(atPath: postboxPath, isDirectory: &isPostboxDir), isPostboxDir.boolValue { + // Delete postbox/db + let dbPath = (postboxPath as NSString).appendingPathComponent("db") + var isDbDir: ObjCBool = false + if fileManager.fileExists(atPath: dbPath, isDirectory: &isDbDir), isDbDir.boolValue { + NSLog("[SG.DBReset] Trying to delete postbox/db in: \(accountName)") + try fileManager.removeItem(atPath: dbPath) + NSLog("[SG.DBReset] OK. Deleted postbox/db directory in: \(accountName)") + } + + // Delete postbox/media + let mediaPath = (postboxPath as NSString).appendingPathComponent("media") + var isMediaDir: ObjCBool = false + if fileManager.fileExists(atPath: mediaPath, isDirectory: &isMediaDir), isMediaDir.boolValue { + NSLog("[SG.DBReset] Trying to delete postbox/media in: \(accountName)") + try fileManager.removeItem(atPath: mediaPath) + NSLog("[SG.DBReset] OK. Deleted postbox/media directory in: \(accountName)") + } + + deletedPostboxCount += 1 + } + } + + + NSLog("[SG.DBReset] Done. Reset All completed") + let successAlert = UIAlertController( + title: "Reset All completed", + message: nil, + preferredStyle: .alert + ) + successAlert.addAction(UIAlertAction(title: "Restart App", style: .cancel) { _ in + exit(0) + }) + alert.dismiss(animated: false) { + present?(successAlert) + } + } catch { + NSLog("[SG.DBReset] ERROR. Reset All failed: \(error)") + let failAlert = UIAlertController( + title: "ERROR. Reset All failed", + message: "\(error)", + preferredStyle: .alert + ) + alert.dismiss(animated: false) { + present?(failAlert) + } + } + }) + ensureAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + exit(0) + }) + + present?(ensureAlert) + }) + + present?(startAlert) + UserDefaults.standard.set(false, forKey: dbHardResetKey) +} diff --git a/Swiftgram/SGDebugUI/BUILD b/Swiftgram/SGDebugUI/BUILD new file mode 100644 index 0000000000..c3a6130f24 --- /dev/null +++ b/Swiftgram/SGDebugUI/BUILD @@ -0,0 +1,51 @@ +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", + "//Swiftgram/SGSwiftUI:SGSwiftUI", + "//Swiftgram/SGIAP:SGIAP", + "//Swiftgram/SGPayWall:SGPayWall", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/LegacyUI:LegacyUI", + "//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 0000000000..dd852e384c --- /dev/null +++ b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift @@ -0,0 +1,217 @@ +import Foundation +import UniformTypeIdentifiers +import SGItemListUI +import UndoUI +import AccountContext +import Display +import TelegramCore +import Postbox +import ItemListUI +import SwiftSignalKit +import TelegramPresentationData +import PresentationDataUtils +import TelegramUIPreferences + +// Optional +import SGSimpleSettings +import SGLogging +import SGPayWall +import OverlayStatusController +#if DEBUG +import FLEX +#endif + + +private enum SGDebugControllerSection: Int32, SGItemListSection { + case base + case notifications +} + +private enum SGDebugDisclosureLink: String { + case sessionBackupManager + case messageFilter + case debugIAP +} + +private enum SGDebugActions: String { + case flexing + case fileManager + case clearRegDateCache + case restorePurchases + case setIAP + case resetIAP +} + +private enum SGDebugToggles: String { + case forceImmediateShareSheet + case legacyNotificationsFix + case inputToolbar +} + + +private enum SGDebugOneFromManySetting: String { + case pinnedMessageNotifications + case mentionsAndRepliesNotifications +} + +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)) + entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", 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)) + + entries.append(.action(id: id.count, section: .base, actionType: .restorePurchases, text: "PayWall.RestorePurchases".i18n(presentationData.strings.baseLanguageCode), kind: .generic)) + #if DEBUG + entries.append(.action(id: id.count, section: .base, actionType: .setIAP, text: "Set Pro", kind: .generic)) + #endif + entries.append(.action(id: id.count, section: .base, actionType: .resetIAP, text: "Reset Pro", kind: .destructive)) + + entries.append(.toggle(id: id.count, section: .notifications, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[OLD] Fix empty notifications", 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 + case .legacyNotificationsFix: + SGSimpleSettings.shared.legacyNotificationsFix = value + SGSimpleSettings.shared.synchronizeShared() + case .inputToolbar: + SGSimpleSettings.shared.inputToolbar = value + } + }, setOneFromManyValue: { setting in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + let items: [ActionSheetItem] = [] +// var items: [ActionSheetItem] = [] + +// switch (setting) { +// } + + 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: { _ in + }, 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 + case .fileManager: + #if DEBUG + let baseAppBundleId = Bundle.main.bundleIdentifier! + let appGroupName = "group.\(baseAppBundleId)" + let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) + if let maybeAppGroupUrl = maybeAppGroupUrl { + if let fileManager = FLEXFileBrowserController(path: maybeAppGroupUrl.path) { + FLEXManager.shared.showExplorer() + let flexNavigation = FLEXNavigationController(rootViewController: fileManager) + FLEXManager.shared.presentTool({ return flexNavigation }) + } + } else { + presentControllerImpl?(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Empty path", timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ), + nil) + } + #endif + case .restorePurchases: + presentControllerImpl?(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(args: context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode), timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ), + nil) + context.sharedContext.SGIAP?.restorePurchases {} + case .setIAP: + #if DEBUG + #endif + case .resetIAP: + let updateSettingsSignal = updateSGStatusInteractively(accountManager: context.sharedContext.accountManager, { status in + var status = status + status.status = SGStatus.default.status + SGSimpleSettings.shared.primaryUserId = "" + return status + }) + let _ = (updateSettingsSignal |> deliverOnMainQueue).start(next: { + presentControllerImpl?(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Status reset completed. You can now restore purchases.", timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ), + nil) + }) + } + }) + + 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 0000000000..8a1446f3f1 --- /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 0000000000..abf7df3357 --- /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 0000000000..ac9be00d70 --- /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 0000000000..2cefa9b847 --- /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 0000000000..8742867603 --- /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 0000000000..8d582084e2 --- /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/SGIAP/BUILD b/Swiftgram/SGIAP/BUILD new file mode 100644 index 0000000000..c80d97254b --- /dev/null +++ b/Swiftgram/SGIAP/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGIAP", + module_name = "SGIAP", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGConfig:SGConfig", + "//submodules/AppBundle:AppBundle", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGIAP/Sources/SGIAP.swift b/Swiftgram/SGIAP/Sources/SGIAP.swift new file mode 100644 index 0000000000..c2940161b4 --- /dev/null +++ b/Swiftgram/SGIAP/Sources/SGIAP.swift @@ -0,0 +1,384 @@ +import StoreKit +import SGConfig +import SGLogging +import AppBundle +import Combine + +private final class CurrencyFormatterEntry { + public let symbol: String + public let thousandsSeparator: String + public let decimalSeparator: String + public let symbolOnLeft: Bool + public let spaceBetweenAmountAndSymbol: Bool + public let decimalDigits: Int + + public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + self.symbol = symbol + self.thousandsSeparator = thousandsSeparator + self.decimalSeparator = decimalSeparator + self.symbolOnLeft = symbolOnLeft + self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol + self.decimalDigits = decimalDigits + } +} + +private func getCurrencyExp(currency: String) -> Int { + switch currency { + case "CLF": + return 4 + case "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND": + return 3 + case "BIF", "BYR", "CLP", "CVE", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "UYI", "VND", "VUV", "XAF", "XOF", "XPF": + return 0 + case "MRO": + return 1 + default: + return 2 + } +} + +private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] { + guard let filePath = getAppBundle().path(forResource: "currencies", ofType: "json") else { + return [:] + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [:] + } + + guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else { + return [:] + } + + var result: [String: CurrencyFormatterEntry] = [:] + + for (code, contents) in dict { + if let contentsDict = contents as? [String: AnyObject] { + let entry = CurrencyFormatterEntry( + symbol: contentsDict["symbol"] as! String, + thousandsSeparator: contentsDict["thousandsSeparator"] as! String, + decimalSeparator: contentsDict["decimalSeparator"] as! String, + symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue, + spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue, + decimalDigits: getCurrencyExp(currency: code.uppercased()) + ) + result[code] = entry + result[code.lowercased()] = entry + } + } + + return result +} + +private let currencyFormatterEntries = loadCurrencyFormatterEntries() + +private func fractionalValueToCurrencyAmount(value: Double, currency: String) -> Int64? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + if value > Double(Int64.max) / factor { + return nil + } else { + return Int64(value * factor) + } +} + + +public extension Notification.Name { + static let SGIAPHelperPurchaseNotification = Notification.Name("SGIAPPurchaseNotification") + static let SGIAPHelperErrorNotification = Notification.Name("SGIAPErrorNotification") + static let SGIAPHelperProductsUpdatedNotification = Notification.Name("SGIAPProductsUpdatedNotification") + static let SGIAPHelperValidationErrorNotification = Notification.Name("SGIAPValidationErrorNotification") +} + +public final class SGIAPManager: NSObject { + private var productRequest: SKProductsRequest? + private var productsRequestCompletion: (([SKProduct]) -> Void)? + private var purchaseCompletion: ((Bool, Error?) -> Void)? + + public private(set) var availableProducts: [SGProduct] = [] + private var finishedSuccessfulTransactions = Set() + private var onRestoreCompletion: (() -> Void)? + + public final class SGProduct: Equatable { + private lazy var numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .currency + numberFormatter.locale = self.skProduct.priceLocale + return numberFormatter + }() + + public let skProduct: SKProduct + + init(skProduct: SKProduct) { + self.skProduct = skProduct + } + + public var id: String { + return self.skProduct.productIdentifier + } + + public var isSubscription: Bool { + if #available(iOS 12.0, *) { + return self.skProduct.subscriptionGroupIdentifier != nil + } else { + return self.skProduct.subscriptionPeriod != nil + } + } + + public var price: String { + return self.numberFormatter.string(from: self.skProduct.price) ?? "" + } + + public func pricePerMonth(_ monthsCount: Int) -> String { + let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2) + return self.numberFormatter.string(from: price) ?? "" + } + + public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String { + let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2) + let prettierPrice = price + .multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .up, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + return self.numberFormatter.string(from: prettierPrice) ?? "" + } + + public func multipliedPrice(count: Int) -> String { + let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2) + let prettierPrice = price + .multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .up, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + return self.numberFormatter.string(from: prettierPrice) ?? "" + } + + public var priceValue: NSDecimalNumber { + return self.skProduct.price + } + + public var priceCurrencyAndAmount: (currency: String, amount: Int64) { + if let currencyCode = self.numberFormatter.currencyCode, + let amount = fractionalValueToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) { + return (currencyCode, amount) + } else { + return ("", 0) + } + } + + public static func ==(lhs: SGProduct, rhs: SGProduct) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.isSubscription != rhs.isSubscription { + return false + } + if lhs.priceValue != rhs.priceValue { + return false + } + return true + } + + } + + public init(foo: Bool = false) { // I don't want to override init, idk why + super.init() + + SKPaymentQueue.default().add(self) + + #if DEBUG && false + DispatchQueue.main.asyncAfter(deadline: .now() + 20) { + self.requestProducts() + } + #else + self.requestProducts() + #endif + } + + deinit { + SKPaymentQueue.default().remove(self) + } + + public var canMakePayments: Bool { + return SKPaymentQueue.canMakePayments() + } + + public func buyProduct(_ product: SKProduct) { + SGLogger.shared.log("SGIAP", "Buying \(product.productIdentifier)...") + let payment = SKPayment(product: product) + SKPaymentQueue.default().add(payment) + } + + private func requestProducts() { + SGLogger.shared.log("SGIAP", "Requesting products for \(SG_CONFIG.iaps.count) ids...") + let productRequest = SKProductsRequest(productIdentifiers: Set(SG_CONFIG.iaps)) + + productRequest.delegate = self + productRequest.start() + + self.productRequest = productRequest + } + + public func restorePurchases(completion: @escaping () -> Void) { + SGLogger.shared.log("SGIAP", "Restoring purchases...") + self.onRestoreCompletion = completion + + let paymentQueue = SKPaymentQueue.default() + paymentQueue.restoreCompletedTransactions() + } + +} + +extension SGIAPManager: SKProductsRequestDelegate { + public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + self.productRequest = nil + + DispatchQueue.main.async { + let products = response.products + SGLogger.shared.log("SGIAP", "Received products (\(products.count)): \(products.map({ $0.productIdentifier }).joined(separator: ", "))") + let currentlyAvailableProducts = self.availableProducts + self.availableProducts = products.map({ SGProduct(skProduct: $0) }) + if currentlyAvailableProducts != self.availableProducts { + NotificationCenter.default.post(name: .SGIAPHelperProductsUpdatedNotification, object: nil) + } + } + } + + public func request(_ request: SKRequest, didFailWithError error: Error) { + SGLogger.shared.log("SGIAP", "Failed to load list of products. Error \(error.localizedDescription)") + self.productRequest = nil + } +} + +extension SGIAPManager: SKPaymentTransactionObserver { + public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + SGLogger.shared.log("SGIAP", "paymentQueue transactions \(transactions.count)") + var purchaceTransactions: [SKPaymentTransaction] = [] + for transaction in transactions { + SGLogger.shared.log("SGIAP", "Transaction \(transaction.transactionIdentifier ?? "nil") state for product \(transaction.payment.productIdentifier): \(transaction.transactionState.description)") + switch transaction.transactionState { + case .purchased, .restored: + purchaceTransactions.append(transaction) + break + case .purchasing, .deferred: + // Ignoring + break + case .failed: + var localizedError: String = "" + if let transactionError = transaction.error as NSError?, + let localizedDescription = transaction.error?.localizedDescription, + transactionError.code != SKError.paymentCancelled.rawValue { + localizedError = localizedDescription + SGLogger.shared.log("SGIAP", "Transaction Error [\(transaction.transactionIdentifier ?? "nil")]: \(localizedDescription)") + } + SGLogger.shared.log("SGIAP", "Sending SGIAPHelperErrorNotification for \(transaction.transactionIdentifier ?? "nil")") + NotificationCenter.default.post(name: .SGIAPHelperErrorNotification, object: transaction, userInfo: ["localizedError": localizedError]) + default: + SGLogger.shared.log("SGIAP", "Unknown transaction \(transaction.transactionIdentifier ?? "nil") state \(transaction.transactionState). Finishing transaction.") + SKPaymentQueue.default().finishTransaction(transaction) + } + } + + if !purchaceTransactions.isEmpty { + SGLogger.shared.log("SGIAP", "Sending SGIAPHelperPurchaseNotification for \(purchaceTransactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + NotificationCenter.default.post(name: .SGIAPHelperPurchaseNotification, object: purchaceTransactions) + } + } + + public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { + SGLogger.shared.log("SGIAP", "Transactions restored") + + if let onRestoreCompletion = self.onRestoreCompletion { + self.onRestoreCompletion = nil + onRestoreCompletion() + } + } + +} + +private extension NSDecimalNumber { + func round(_ decimals: Int) -> NSDecimalNumber { + return self.rounding(accordingToBehavior: + NSDecimalNumberHandler(roundingMode: .down, + scale: Int16(decimals), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false)) + } + + func prettyPrice() -> NSDecimalNumber { + return self.multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .plain, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + } +} + + +public func getPurchaceReceiptData() -> Data? { + var receiptData: Data? + if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { + do { + receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) + } catch { + SGLogger.shared.log("SGIAP", "Couldn't read receipt data with error: \(error.localizedDescription)") + } + } else { + SGLogger.shared.log("SGIAP", "Couldn't find receipt path") + } + return receiptData +} + + +extension SKPaymentTransactionState { + var description: String { + switch self { + case .purchasing: + return "Purchasing" + case .purchased: + return "Purchased" + case .failed: + return "Failed" + case .restored: + return "Restored" + case .deferred: + return "Deferred" + @unknown default: + return "Unknown" + } + } +} + diff --git a/Swiftgram/SGIQTP/BUILD b/Swiftgram/SGIQTP/BUILD new file mode 100644 index 0000000000..99dbb60303 --- /dev/null +++ b/Swiftgram/SGIQTP/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGIQTP", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGIQTP/Sources/SGIQTP.swift b/Swiftgram/SGIQTP/Sources/SGIQTP.swift new file mode 100644 index 0000000000..5063537930 --- /dev/null +++ b/Swiftgram/SGIQTP/Sources/SGIQTP.swift @@ -0,0 +1,77 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit +import SGConfig +import SGLogging + + +public struct SGIQTPResponse { + public let status: Int + public let description: String? + public let text: String? +} + +public func makeIqtpQuery(_ api: Int, _ method: String, _ args: [String] = []) -> String { + let buildNumber = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] ?? "" + let baseQuery = "tp:\(api):\(buildNumber):\(method)" + if args.isEmpty { + return baseQuery + } + return baseQuery + ":" + args.joined(separator: ":") +} + +public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { + let queryId = arc4random() + #if DEBUG + SGLogger.shared.log("SGIQTP", "[\(queryId)] Query: \(query)") + #else + SGLogger.shared.log("SGIQTP", "[\(queryId)] Query") + #endif + return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to resolve peer \(SG_CONFIG.botUsername)") + return .complete() + } + return .single(result) + } + |> mapToSignal { peer -> Signal in + guard let peer = peer else { + SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty peer") + return .single(nil) + } + return engine.messages.requestChatContextResults(IQTP: true, botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) + |> map { results -> ChatContextResultCollection? in + return results?.results + } + |> `catch` { error -> Signal in + SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to request inline results") + return .single(nil) + } + } + |> map { contextResult -> SGIQTPResponse? in + guard let contextResult, let firstResult = contextResult.results.first else { + SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty inline result") + return nil + } + + var t: String? + if case let .text(text, _, _, _, _) = firstResult.message { + t = text + } + + var status = 400 + if let title = firstResult.title { + status = Int(title) ?? 400 + } + let response = SGIQTPResponse( + status: status, + description: firstResult.description, + text: t + ) + SGLogger.shared.log("SGIQTP", "[\(queryId)] Response: \(response)") + return response + } +} diff --git a/Swiftgram/SGInputToolbar/BUILD b/Swiftgram/SGInputToolbar/BUILD new file mode 100644 index 0000000000..6b2de974a1 --- /dev/null +++ b/Swiftgram/SGInputToolbar/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGInputToolbar", + module_name = "SGInputToolbar", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGInputToolbar/Sources/SGInputToolbar.swift b/Swiftgram/SGInputToolbar/Sources/SGInputToolbar.swift new file mode 100644 index 0000000000..a21f317e93 --- /dev/null +++ b/Swiftgram/SGInputToolbar/Sources/SGInputToolbar.swift @@ -0,0 +1,148 @@ +import SwiftUI +import Foundation + + +// MARK: Swiftgram +@available(iOS 13.0, *) +public struct ChatToolbarView: View { + var onQuote: () -> Void + var onSpoiler: () -> Void + var onBold: () -> Void + var onItalic: () -> Void + var onMonospace: () -> Void + var onLink: () -> Void + var onStrikethrough: () -> Void + var onUnderline: () -> Void + var onCode: () -> Void + + var onNewLine: () -> Void + @Binding private var showNewLine: Bool + + var onClearFormatting: () -> Void + + public init( + onQuote: @escaping () -> Void, + onSpoiler: @escaping () -> Void, + onBold: @escaping () -> Void, + onItalic: @escaping () -> Void, + onMonospace: @escaping () -> Void, + onLink: @escaping () -> Void, + onStrikethrough: @escaping () -> Void, + onUnderline: @escaping () -> Void, + onCode: @escaping () -> Void, + onNewLine: @escaping () -> Void, + showNewLine: Binding, + onClearFormatting: @escaping () -> Void + ) { + self.onQuote = onQuote + self.onSpoiler = onSpoiler + self.onBold = onBold + self.onItalic = onItalic + self.onMonospace = onMonospace + self.onLink = onLink + self.onStrikethrough = onStrikethrough + self.onUnderline = onUnderline + self.onCode = onCode + self.onNewLine = onNewLine + self._showNewLine = showNewLine + self.onClearFormatting = onClearFormatting + } + + public func setShowNewLine(_ value: Bool) { + self.showNewLine = value + } + + public var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + if showNewLine { + Button(action: onNewLine) { + Image(systemName: "return") + } + .buttonStyle(ToolbarButtonStyle()) + } + Button(action: onClearFormatting) { + Image(systemName: "pencil.slash") + } + .buttonStyle(ToolbarButtonStyle()) + Spacer() + // Quote Button + Button(action: onQuote) { + Image(systemName: "text.quote") + } + .buttonStyle(ToolbarButtonStyle()) + + // Spoiler Button + Button(action: onSpoiler) { + Image(systemName: "eye.slash") + } + .buttonStyle(ToolbarButtonStyle()) + + // Bold Button + Button(action: onBold) { + Image(systemName: "bold") + } + .buttonStyle(ToolbarButtonStyle()) + + // Italic Button + Button(action: onItalic) { + Image(systemName: "italic") + } + .buttonStyle(ToolbarButtonStyle()) + + // Monospace Button + Button(action: onMonospace) { + if #available(iOS 16.4, *) { + Text("M").monospaced() + } else { + Text("M") + } + } + .buttonStyle(ToolbarButtonStyle()) + + // Link Button + Button(action: onLink) { + Image(systemName: "link") + } + .buttonStyle(ToolbarButtonStyle()) + + // Underline Button + Button(action: onUnderline) { + Image(systemName: "underline") + } + .buttonStyle(ToolbarButtonStyle()) + + + // Strikethrough Button + Button(action: onStrikethrough) { + Image(systemName: "strikethrough") + } + .buttonStyle(ToolbarButtonStyle()) + + + // Code Button + Button(action: onCode) { + Image(systemName: "chevron.left.forwardslash.chevron.right") + } + .buttonStyle(ToolbarButtonStyle()) + } + .padding(.horizontal, 8) + .padding(.vertical, 8) + } + .background(Color(UIColor.clear)) + } +} + +@available(iOS 13.0, *) +struct ToolbarButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 17)) + .frame(width: 36, height: 36, alignment: .center) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(8) + // TODO(swiftgram): Does not work for fast taps (like mine) + .opacity(configuration.isPressed ? 0.4 : 1.0) + } +} diff --git a/Swiftgram/SGItemListUI/BUILD b/Swiftgram/SGItemListUI/BUILD new file mode 100644 index 0000000000..d0dd458986 --- /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 0000000000..78d802eac2 --- /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 0000000000..ad61f47bba --- /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/SGKeychainBackupManager/BUILD b/Swiftgram/SGKeychainBackupManager/BUILD new file mode 100644 index 0000000000..cd1a5d1293 --- /dev/null +++ b/Swiftgram/SGKeychainBackupManager/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGKeychainBackupManager", + module_name = "SGKeychainBackupManager", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGKeychainBackupManager/Sources/SGKeychainBackupManager.swift b/Swiftgram/SGKeychainBackupManager/Sources/SGKeychainBackupManager.swift new file mode 100644 index 0000000000..2de2ebd5ed --- /dev/null +++ b/Swiftgram/SGKeychainBackupManager/Sources/SGKeychainBackupManager.swift @@ -0,0 +1,131 @@ +import Foundation +import Security + +public enum KeychainError: Error { + case duplicateEntry + case unknown(OSStatus) + case itemNotFound + case invalidItemFormat +} + +public class KeychainBackupManager { + public static let shared = KeychainBackupManager() + private let service = "\(Bundle.main.bundleIdentifier!).sessionsbackup" + + private init() {} + + // MARK: - Save Credentials + public func saveSession(id: String, _ session: Data) throws { + // Create query dictionary + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: id, + kSecValueData as String: session, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked + ] + + // Add to keychain + let status = SecItemAdd(query as CFDictionary, nil) + + if status == errSecDuplicateItem { + // Item already exists, update it + let updateQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: id + ] + + let attributesToUpdate: [String: Any] = [ + kSecValueData as String: session + ] + + let updateStatus = SecItemUpdate(updateQuery as CFDictionary, + attributesToUpdate as CFDictionary) + + if updateStatus != errSecSuccess { + throw KeychainError.unknown(updateStatus) + } + } else if status != errSecSuccess { + throw KeychainError.unknown(status) + } + } + + // MARK: - Retrieve Credentials + public func retrieveSession(for id: String) throws -> Data { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: id, + kSecReturnData as String: true + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess, let sessionData = result as? Data else { + throw KeychainError.itemNotFound + } + + return sessionData + } + + // MARK: - Delete Credentials + public func deleteSession(for id: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: id + ] + + let status = SecItemDelete(query as CFDictionary) + + if status != errSecSuccess && status != errSecItemNotFound { + throw KeychainError.unknown(status) + } + } + + // MARK: - Retrieve All Accounts + public func getAllSessons() throws -> [Data] { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitAll + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + if status == errSecItemNotFound { + return [] + } + + guard status == errSecSuccess, + let credentialsDataArray = result as? [Data] else { + throw KeychainError.unknown(status) + } + + return credentialsDataArray + } + + // MARK: - Delete All Sessions + public func deleteAllSessions() throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service + ] + + let status = SecItemDelete(query as CFDictionary) + + // If no items were found, that's fine - just return + if status == errSecItemNotFound { + return + } + + // For any other error, throw + if status != errSecSuccess { + throw KeychainError.unknown(status) + } + } +} diff --git a/Swiftgram/SGLogging/BUILD b/Swiftgram/SGLogging/BUILD new file mode 100644 index 0000000000..498396974c --- /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 0000000000..22a88f02a8 --- /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 0000000000..68381110b1 --- /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/SGPayWall/BUILD b/Swiftgram/SGPayWall/BUILD new file mode 100644 index 0000000000..d822f25b09 --- /dev/null +++ b/Swiftgram/SGPayWall/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +filegroup( + name = "SGPayWallAssets", + srcs = glob(["Images.xcassets/**"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "SGPayWall", + module_name = "SGPayWall", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGIAP:SGIAP", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGSwiftUI:SGSwiftUI", + "//Swiftgram/SGStrings:SGStrings", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGPayWall/Images.xcassets/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Backup.png b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Backup.png new file mode 100644 index 0000000000..06fbd1ff95 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Backup.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Contents.json new file mode 100644 index 0000000000..558d4c7886 --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsBackup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Backup.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Contents.json new file mode 100644 index 0000000000..7c01e065c8 --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Filter.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Filter.png b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Filter.png new file mode 100644 index 0000000000..665a294eba Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFilter.imageset/Filter.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Contents.json new file mode 100644 index 0000000000..2c6fae7057 --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Formatting.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Formatting.png b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Formatting.png new file mode 100644 index 0000000000..9945739af2 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsFormatting.imageset/Formatting.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Contents.json new file mode 100644 index 0000000000..7a1f30c724 --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Icons.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Icons.png b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Icons.png new file mode 100644 index 0000000000..300ec522b6 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsIcons.imageset/Icons.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Contents.json new file mode 100644 index 0000000000..3527b118da --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Mute.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Mute.png b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Mute.png new file mode 100644 index 0000000000..05e22393c7 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/ProDetailsMute.imageset/Mute.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/Contents.json b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/Contents.json new file mode 100644 index 0000000000..6c7c64d61c --- /dev/null +++ b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pro.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pro@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pro@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro.png b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro.png new file mode 100644 index 0000000000..56e048db07 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@2x.png b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@2x.png new file mode 100644 index 0000000000..c76f57d4e7 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@2x.png differ diff --git a/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@3x.png b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@3x.png new file mode 100644 index 0000000000..942d676df9 Binary files /dev/null and b/Swiftgram/SGPayWall/Images.xcassets/pro.imageset/pro@3x.png differ diff --git a/Swiftgram/SGPayWall/Sources/SGPayWall.swift b/Swiftgram/SGPayWall/Sources/SGPayWall.swift new file mode 100644 index 0000000000..2a1ae85504 --- /dev/null +++ b/Swiftgram/SGPayWall/Sources/SGPayWall.swift @@ -0,0 +1,993 @@ +import Foundation +import SwiftUI +import StoreKit +import SGSwiftUI +import SGIAP +import TelegramPresentationData +import LegacyUI +import Display +import SGConfig +import SGStrings +import SwiftSignalKit +import TelegramUIPreferences + + +@available(iOS 13.0, *) +public func sgPayWallController(statusSignal: Signal, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String, Bool) -> Void /* url, forceExternal */, paymentsEnabled: Bool, canBuyInBeta: Bool, openAppStorePage: @escaping () -> Void, proSupportUrl: String?) -> ViewController { + // let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme) + let theme = defaultDarkColorPresentationTheme + let strings = presentationData?.strings ?? defaultPresentationStrings + + let legacyController = LegacySwiftUIController( + presentation: .modal(animateIn: true), + theme: theme, + strings: strings + ) + // legacyController.displayNavigationBar = false + legacyController.statusBar.statusBarStyle = .White + legacyController.attemptNavigation = { _ in return false } + legacyController.view.disablesInteractiveTransitionGestureRecognizer = true + + let swiftUIView = SGSwiftUIView( + legacyController: legacyController, + content: { + SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, openUrl: openUrl, openAppStorePage: openAppStorePage, paymentsEnabled: paymentsEnabled, canBuyInBeta: canBuyInBeta, proSupportUrl: proSupportUrl) + } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) + + return legacyController +} + +private let innerShadowWidth: CGFloat = 15.0 +private let accentColorHex: String = "F1552E" + + +@available(iOS 13.0, *) +struct BackgroundView: View { + + var body: some View { + ZStack { + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color(hex: "A053F8").opacity(0.8), location: 0.0), // purple gradient + .init(color: Color.clear, location: 0.20), + + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .edgesIgnoringSafeArea(.all) + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color(hex: "CC4303").opacity(0.6), location: 0.0), // orange gradient + .init(color: Color.clear, location: 0.15), + ]), + startPoint: .topTrailing, + endPoint: .bottomLeading + ) + .blendMode(.lighten) + + .edgesIgnoringSafeArea(.all) + .overlay( + RoundedRectangle(cornerRadius: 0) + .stroke(Color.clear, lineWidth: 0) + .background( + ZStack { + innerShadow(x: -2, y: -2, blur: 4, color: Color(hex: "FF8C56")) // orange shadow + innerShadow(x: 2, y: 2, blur: 4, color: Color(hex: "A053F8")) // purple shadow + // innerShadow(x: 0, y: 0, blur: 4, color: Color.white.opacity(0.3)) + } + ) + ) + .edgesIgnoringSafeArea(.all) + } + .background(Color.black) + } + + func innerShadow(x: CGFloat, y: CGFloat, blur: CGFloat, color: Color) -> some View { + return RoundedRectangle(cornerRadius: 0) + .stroke(color, lineWidth: innerShadowWidth) + .blur(radius: blur) + .offset(x: x, y: y) + .mask(RoundedRectangle(cornerRadius: 0).fill(LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]), startPoint: .top, endPoint: .bottom))) + } +} + + +@available(iOS 13.0, *) +struct SGPayWallFeatureDetails: View { + + let dismissAction: () -> Void + var bottomOffset: CGFloat = 0.0 + let contentHeight: CGFloat = 690.0 + let features: [SGProFeature] + + @State var shownFeature: SGProFeatureId? + // Add animation states + @State private var showBackground = false + @State private var showContent = false + + @State private var dragOffset: CGFloat = 0 + + var body: some View { + ZStack(alignment: .bottom) { + // Background overlay + if showBackground { + Color.black.opacity(0.4) + .zIndex(0) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + dismissWithAnimation() + } + .transition(.opacity) + } + + // Bottom sheet content + if showContent { + VStack { + if #available(iOS 14.0, *) { + TabView(selection: $shownFeature) { + ForEach(features) { feature in + ScrollView(showsIndicators: false) { + SGProFeatureView( + feature: feature + ) + Color.clear.frame(height: 8.0) // paginator padding + } + .tag(feature.id) + .scrollBounceBehaviorIfAvailable(.basedOnSize) + } + } + .tabViewStyle(.page) + .padding(.bottom, bottomOffset - 8.0) + } + + // Spacer for purchase buttons + if !bottomOffset.isZero { + Color.clear.frame(height: bottomOffset) + } + } + .zIndex(1) + .frame(maxHeight: contentHeight) + .background(Color(.black)) + .cornerRadius(8, corners: [.topLeft, .topRight]) + .overlay(closeButtonView) + .offset(y: max(0, dragOffset)) + .gesture( + DragGesture() + .onChanged { value in + // Only track downward movement + if value.translation.height > 0 { + dragOffset = value.translation.height + } + } + .onEnded { value in + // If dragged down more than 150 points or with significant velocity, dismiss + if value.translation.height > 150 || value.predictedEndTranslation.height > 200 { + dismissWithAnimation() + } else { + // Otherwise, reset position + withAnimation(.spring()) { + dragOffset = 0 + } + } + } + ) + .transition(.move(edge: .bottom)) + } + } + .onAppear { + appearWithAnimation() + } + } + + private func appearWithAnimation() { + withAnimation(.easeIn(duration: 0.2)) { + showBackground = true + } + + withAnimation(.spring(duration: 0.3)/*.delay(0.1)*/) { + showContent = true + } + } + + private func dismissWithAnimation() { + withAnimation(.spring()) { + showContent = false + dragOffset = 0 + } + + withAnimation(.easeOut(duration: 0.2).delay(0.1)) { + showBackground = false + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + dismissAction() + } + } + + private var closeButtonView: some View { + Button(action: { + dismissWithAnimation() + }) { + Image(systemName: "xmark") + .font(.headline) + .foregroundColor(.secondary.opacity(0.6)) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) // Improve tappable area + } + .opacity(showContent ? 1.0 : 0.0) + .padding([.top, .trailing], 8) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + } +} + + +@available(iOS 13.0, *) +struct SGProFeatureView: View { + let feature: SGProFeature + + var body: some View { + VStack(spacing: 16) { + feature.image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, maxHeight: 400.0, alignment: .top) + .clipped() + + VStack(alignment: .center, spacing: 8) { + Text(feature.title) + .font(.title) + .fontWeight(.bold) + .multilineTextAlignment(.center) + Text(featureSubtitle) + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.horizontal) + + Spacer() + } + } + + var featureSubtitle: String { + return feature.description ?? feature.subtitle + } +} + +enum SGProFeatureId: Hashable { + case backup + case filter + case notifications + case toolbar + case icons +} + + +@available(iOS 13.0, *) +struct SGProFeature: Identifiable { + + let id: SGProFeatureId + let title: String + let subtitle: String + let description: String? + + @ViewBuilder + public var icon: some View { + switch (id) { + case .backup: + FeatureIcon(icon: "lock.fill", backgroundColor: .blue) + case .filter: + FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold) + case .notifications: + FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red) + case .toolbar: + FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16) + case .icons: + Image("SwiftgramSettings") + .resizable() + .frame(width: 32, height: 32) + @unknown default: + Image("SwiftgramPro") + .resizable() + .frame(width: 32, height: 32) + } + } + + public var image: Image { + switch (id) { + case .backup: + return Image("ProDetailsBackup") + case .filter: + return Image("ProDetailsFilter") + case .notifications: + return Image("ProDetailsMute") + case .toolbar: + return Image("ProDetailsFormatting") + case .icons: + return Image("ProDetailsIcons") + @unknown default: + return Image("pro") + } + } +} + + +@available(iOS 13.0, *) +struct SGPayWallView: View { + @Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat + @Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout? + @Environment(\.lang) var lang: String + + weak var wrapperController: LegacyController? + let replacementController: ViewController + let SGIAP: SGIAPManager + let statusSignal: Signal + let openUrl: (String, Bool) -> Void // url, forceExternal + let openAppStorePage: () -> Void + let paymentsEnabled: Bool + let canBuyInBeta: Bool + let proSupportUrl: String? + + private enum PayWallState: Equatable { + case ready // ready to buy + case restoring + case purchasing + case validating + } + + // State management + @State private var product: SGIAPManager.SGProduct? + @State private var currentStatus: Int64 = 1 + @State private var state: PayWallState = .ready + @State private var showErrorAlert: Bool = false + @State private var showConfetti: Bool = false + @State private var showDetails: Bool = false + @State private var shownFeature: SGProFeatureId? = nil + + private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil) + private let buyOrRestoreSuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil) + private let buyErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperErrorNotification, object: nil) + private let validationErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperValidationErrorNotification, object: nil) + + @State private var statusTask: Task? = nil + + @State private var hapticFeedback: HapticFeedback? + private let confettiDuration: Double = 5.0 + + @State private var purchaseSectionSize: CGSize = .zero + + private var features: [SGProFeature] { + return [ + SGProFeature(id: .backup, title: "PayWall.SessionBackup.Title".i18n(lang), subtitle: "PayWall.SessionBackup.Notice".i18n(lang), description: "PayWall.SessionBackup.Description".i18n(lang)), + SGProFeature(id: .filter, title: "PayWall.MessageFilter.Title".i18n(lang), subtitle: "PayWall.MessageFilter.Notice".i18n(lang), description: "PayWall.MessageFilter.Description".i18n(lang)), + SGProFeature(id: .notifications, title: "PayWall.Notifications.Title".i18n(lang), subtitle: "PayWall.Notifications.Notice".i18n(lang), description: "PayWall.Notifications.Description".i18n(lang)), + SGProFeature(id: .toolbar, title: "PayWall.InputToolbar.Title".i18n(lang), subtitle: "PayWall.InputToolbar.Notice".i18n(lang), description: "PayWall.InputToolbar.Description".i18n(lang)), + SGProFeature(id: .icons, title: "PayWall.AppIcons.Title".i18n(lang), subtitle: "PayWall.AppIcons.Notice".i18n(lang), description: nil) + ] + } + + var body: some View { + ZStack { + BackgroundView() + + ZStack(alignment: .bottom) { + ScrollView(showsIndicators: false) { + VStack(spacing: 24) { + // Icon + Image("pro") + .frame(width: 100, height: 100) + + // Title and Subtitle + VStack(spacing: 8) { + Text("Swiftgram Pro") + .font(.largeTitle) + .fontWeight(.bold) + + Text("PayWall.Text".i18n(lang)) + .font(.callout) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + + // Features + VStack(spacing: 36) { + featuresSection + + aboutSection + + VStack(spacing: 8) { + HStack { + legalSection + Spacer() + } + + HStack { + restorePurchasesButton + Spacer() + } + } + } + + + // Spacer for purchase buttons + Color.clear.frame(height: (purchaseSectionSize.height / 2.0)) + } + .padding(.vertical, (purchaseSectionSize.height / 2.0)) + } + .padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout))) + .padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout))) + + if showDetails { + SGPayWallFeatureDetails( + dismissAction: dismissDetails, + bottomOffset: (purchaseSectionSize.height / 2.0) * 0.9, // reduced offset for paginator + features: features, + shownFeature: shownFeature) + } + + // Fixed purchase button at bottom + purchaseSection + .trackSize($purchaseSectionSize) + } + } + .confetti(isActive: $showConfetti, duration: confettiDuration) + .overlay(closeButtonView) + .colorScheme(.dark) + .onReceive(productsPub) { _ in + updateSelectedProduct() + } + .onAppear { + hapticFeedback = HapticFeedback() + updateSelectedProduct() + statusTask = Task { + let statusStream = statusSignal.awaitableStream() + for await newStatus in statusStream { + #if DEBUG + print("SGPayWallView: newStatus = \(newStatus)") + #endif + if Task.isCancelled { + #if DEBUG + print("statusTask cancelled") + #endif + break + } + + if currentStatus != newStatus { + currentStatus = newStatus + + if newStatus > 1 { + handleUpgradedStatus() + } + } + } + } + } + .onDisappear { + #if DEBUG + print("Cancelling statusTask") + #endif + statusTask?.cancel() + } + .onReceive(buyOrRestoreSuccessPub) { _ in + state = .validating + } + .onReceive(buyErrorPub) { notification in + if let userInfo = notification.userInfo, let error = userInfo["localizedError"] as? String, !error.isEmpty { + showErrorAlert(error) + } + } + .onReceive(validationErrorPub) { notification in + if state == .validating { + if let userInfo = notification.userInfo, let error = userInfo["error"] as? String, !error.isEmpty { + showErrorAlert(error.i18n(lang)) + } else { + showErrorAlert("PayWall.ValidationError".i18n(lang)) + } + } + } + } + + private var featuresSection: some View { + VStack(spacing: 8) { + ForEach(features) { feature in + FeatureRow( + icon: feature.icon, + title: feature.title, + subtitle: feature.subtitle, + action: { + showDetailsForFeature(feature.id) + } + ) + } + } + } + + private var restorePurchasesButton: some View { + Button(action: handleRestorePurchases) { + Text("PayWall.RestorePurchases".i18n(lang)) + .font(.footnote) + .fontWeight(.semibold) + .foregroundColor(Color(hex: accentColorHex)) + } + .disabled(state == .restoring || product == nil) + .opacity((state == .restoring || product == nil) ? 0.5 : 1.0) + } + + private var purchaseSection: some View { + VStack(spacing: 0) { + Divider() + VStack(spacing: 8) { + Button(action: handlePurchase) { + Text(buttonTitle) + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + .background(Color(hex: accentColorHex)) + .foregroundColor(.white) + .cornerRadius(12) + } + .disabled((state != .ready || !canPurchase) && !(currentStatus > 1)) + .opacity(((state != .ready || !canPurchase) && !(currentStatus > 1)) ? 0.5 : 1.0) + + if let proSupportUrl = proSupportUrl { + HStack(alignment: .center, spacing: 4) { + Text("PayWall.ProSupport.Title".i18n(lang)) + .font(.caption) + .foregroundColor(.secondary) + Button(action: { + openUrl(proSupportUrl, false) + }) { + Text("PayWall.ProSupport.Contact".i18n(lang)) + .font(.caption) + .foregroundColor(Color(hex: accentColorHex)) + } + } + } + } + .padding([.horizontal, .top]) + .padding(.bottom, sgBottomSafeAreaInset(containerViewLayout) + 2.0) + } + .foregroundColor(Color.black) + .backgroundIfAvailable(material: .ultraThinMaterial) + .shadow(radius: 8, y: -4) + } + + private var legalSection: some View { + Group { + if #available(iOS 15.0, *) { + Text(LocalizedStringKey("PayWall.Notice.Markdown".i18n(lang, args: "PayWall.TermsURL".i18n(lang), "PayWall.PrivacyURL".i18n(lang)))) + .font(.caption) + .tint(Color(hex: accentColorHex)) + .foregroundColor(.secondary) + .environment(\.openURL, OpenURLAction { url in + openUrl(url.absoluteString, false) + return .handled + }) + } else { + Text("PayWall.Notice.Raw".i18n(lang)) + .font(.caption) + .foregroundColor(.secondary) + HStack(alignment: .top, spacing: 8) { + Button(action: { + openUrl("PayWall.PrivacyURL".i18n(lang), true) + }) { + Text("PayWall.Privacy".i18n(lang)) + .font(.caption) + .foregroundColor(Color(hex: accentColorHex)) + } + Button(action: { + openUrl("PayWall.TermsURL".i18n(lang), true) + }) { + Text("PayWall.Terms".i18n(lang)) + .font(.caption) + .foregroundColor(Color(hex: accentColorHex)) + } + } + } + } + } + + + private var aboutSection: some View { + VStack(spacing: 8) { + HStack { + Text("PayWall.About.Title".i18n(lang)) + .font(.headline) + .fontWeight(.medium) + Spacer() + } + + HStack { + Text("PayWall.About.Notice".i18n(lang)) + .font(.subheadline) + .foregroundColor(.secondary) + Spacer() + } + HStack { + Button(action: { + openUrl("PayWall.About.SignatureURL".i18n(lang), false) + }) { + Text("PayWall.About.Signature".i18n(lang)) + .font(.caption) + .foregroundColor(Color(hex: accentColorHex)) + } + Spacer() + } + } + } + + private var closeButtonView: some View { + Button(action: { + wrapperController?.dismiss(animated: true) + }) { + Image(systemName: "xmark") + .font(.headline) + .foregroundColor(.secondary.opacity(0.6)) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) // Improve tappable area + } + .disabled(showDetails) + .opacity(showDetails ? 0.0 : 1.0) + .padding([.top, .trailing], 16) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + } + + private var buttonTitle: String { + if currentStatus > 1 { + return "PayWall.Button.OpenPro".i18n(lang) + } else { + if state == .purchasing { + return "PayWall.Button.Purchasing".i18n(lang) + } else if state == .restoring { + return "PayWall.Button.Restoring".i18n(lang) + } else if state == .validating { + return "PayWall.Button.Validating".i18n(lang) + } else if let product = product { + if !SGIAP.canMakePayments || paymentsEnabled == false { + return "PayWall.Button.PaymentsUnavailable".i18n(lang) + } else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" && !canBuyInBeta { + return "PayWall.Button.BuyInAppStore".i18n(lang) + } else { + return "PayWall.Button.Subscribe".i18n(lang, args: product.price) + } + } else { + return "PayWall.Button.ContactingAppStore".i18n(lang) + } + } + } + + private var canPurchase: Bool { + if !SGIAP.canMakePayments || paymentsEnabled == false { + return false + } else { + return product != nil + } + } + + private func showDetailsForFeature(_ featureId: SGProFeatureId) { + if #available(iOS 14.0, *) { + shownFeature = featureId + showDetails = true + } // pagination is not available on iOS 13 + } + + private func dismissDetails() { +// shownFeature = nil + showDetails = false + } + + private func updateSelectedProduct() { + product = SGIAP.availableProducts.first { $0.id == SG_CONFIG.iaps.first ?? "" } + } + + private func handlePurchase() { + if currentStatus > 1 { + wrapperController?.replace(with: replacementController) + } else { + if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" && !canBuyInBeta { + openAppStorePage() + } else { + guard let product = product else { return } + state = .purchasing + SGIAP.buyProduct(product.skProduct) + } + } + } + + private func handleRestorePurchases() { + state = .restoring + SGIAP.restorePurchases { + state = .validating + } + } + + private func handleUpgradedStatus() { + DispatchQueue.main.async { + hapticFeedback?.success() + showConfetti = true + DispatchQueue.main.asyncAfter(deadline: .now() + confettiDuration + 1.0) { + showConfetti = false + } + } + } + + private func showErrorAlert(_ message: String) { + let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in + state = .ready + })) + DispatchQueue.main.async { + wrapperController?.present(alertController, animated: true) + } + } +} + + +@available(iOS 13.0, *) +struct FeatureIcon: View { + let icon: String + let iconColor: Color + let backgroundColor: Color + let iconSize: CGFloat + let frameSize: CGFloat + let fontWeight: SwiftUI.Font.Weight + + init( + icon: String, + iconColor: Color = .white, + backgroundColor: Color = .blue, + iconSize: CGFloat = 18, + frameSize: CGFloat = 32, + fontWeight: SwiftUI.Font.Weight = .regular + ) { + self.icon = icon + self.iconColor = iconColor + self.backgroundColor = backgroundColor + self.iconSize = iconSize + self.frameSize = frameSize + self.fontWeight = fontWeight + } + + var body: some View { + Image(systemName: icon) + .font(.system(size: iconSize)) + .fontWeightIfAvailable(fontWeight) + .foregroundColor(iconColor) + .frame(width: frameSize, height: frameSize) + .background(backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + + +@available(iOS 13.0, *) +struct FeatureRow: View { + let icon: IconContent + let title: String + let subtitle: String + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 16) { + + HStack(alignment: .top, spacing: 12) { + icon + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + .fontWeight(.medium) + + Text(subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + Spacer() + if #available(iOS 14.0, *) { + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary) + } // Descriptions are not available on iOS 13 + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.systemGray6)) + .shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 4) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} + + + +// Confetti +@available(iOS 13.0, *) +struct ConfettiType { + let color: Color + let shape: ConfettiShape + + static func random() -> ConfettiType { + let colors: [Color] = [.red, .blue, .green, .yellow, .pink, .purple, .orange] + return ConfettiType( + color: colors.randomElement() ?? .blue, + shape: ConfettiShape.allCases.randomElement() ?? .circle + ) + } +} + +@available(iOS 13.0, *) +enum ConfettiShape: CaseIterable { + case circle + case triangle + case square + case slimRectangle + case roundedCross + + @ViewBuilder + func view(color: Color) -> some View { + switch self { + case .circle: + Circle().fill(color) + case .triangle: + Triangle().fill(color) + case .square: + Rectangle().fill(color) + case .slimRectangle: + SlimRectangle().fill(color) + case .roundedCross: + RoundedCross().fill(color) + } + } +} + +@available(iOS 13.0, *) +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.closeSubpath() + return path + } +} + +@available(iOS 13.0, *) +public struct SlimRectangle: Shape { + public func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.minX, y: 4*rect.maxY/5)) + path.addLine(to: CGPoint(x: rect.maxX, y: 4*rect.maxY/5)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + + return path + } +} + +@available(iOS 13.0, *) +public struct RoundedCross: Shape { + public func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.minX, y: rect.maxY/3)) + path.addQuadCurve(to: CGPoint(x: rect.maxX/3, y: rect.minY), control: CGPoint(x: rect.maxX/3, y: rect.maxY/3)) + path.addLine(to: CGPoint(x: 2*rect.maxX/3, y: rect.minY)) + + path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY/3), control: CGPoint(x: 2*rect.maxX/3, y: rect.maxY/3)) + path.addLine(to: CGPoint(x: rect.maxX, y: 2*rect.maxY/3)) + + path.addQuadCurve(to: CGPoint(x: 2*rect.maxX/3, y: rect.maxY), control: CGPoint(x: 2*rect.maxX/3, y: 2*rect.maxY/3)) + path.addLine(to: CGPoint(x: rect.maxX/3, y: rect.maxY)) + + path.addQuadCurve(to: CGPoint(x: 2*rect.minX/3, y: 2*rect.maxY/3), control: CGPoint(x: rect.maxX/3, y: 2*rect.maxY/3)) + + return path + } +} + +@available(iOS 13.0, *) +struct ConfettiModifier: ViewModifier { + @Binding var isActive: Bool + let duration: Double + + func body(content: Content) -> some View { + content.overlay( + ZStack { + if isActive { + ForEach(0..<70) { _ in + ConfettiPiece( + confettiType: .random(), + duration: duration + ) + } + } + } + ) + } +} + +@available(iOS 13.0, *) +struct ConfettiPiece: View { + let confettiType: ConfettiType + let duration: Double + + @State private var isAnimating = false + @State private var rotation = Double.random(in: 0...1080) + + var body: some View { + confettiType.shape.view(color: confettiType.color) + .frame(width: 10, height: 10) + .rotationEffect(.degrees(rotation)) + .position( + x: .random(in: 0...UIScreen.main.bounds.width), + y: 0 //-20 + ) + .modifier(FallingModifier(distance: UIScreen.main.bounds.height + 20, duration: duration)) + .opacity(isAnimating ? 0 : 1) + .onAppear { + withAnimation(.linear(duration: duration)) { + isAnimating = true + } + } + } +} + +@available(iOS 13.0, *) +struct FallingModifier: ViewModifier { + let distance: CGFloat + let duration: Double + + func body(content: Content) -> some View { + content.modifier( + MoveModifier( + offset: CGSize( + width: .random(in: -100...100), + height: distance + ), + duration: duration + ) + ) + } +} + +@available(iOS 13.0, *) +struct MoveModifier: ViewModifier { + let offset: CGSize + let duration: Double + + @State private var isAnimating = false + + func body(content: Content) -> some View { + content.offset( + x: isAnimating ? offset.width : 0, + y: isAnimating ? offset.height : 0 + ) + .onAppear { + withAnimation( + .linear(duration: duration) + .speed(.random(in: 0.5...2.5)) + ) { + isAnimating = true + } + } + } +} + +// Extension to make it easier to use +@available(iOS 13.0, *) +extension View { + func confetti(isActive: Binding, duration: Double = 2.0) -> some View { + modifier(ConfettiModifier(isActive: isActive, duration: duration)) + } +} diff --git a/Swiftgram/SGProUI/BUILD b/Swiftgram/SGProUI/BUILD new file mode 100644 index 0000000000..e748ea0cfe --- /dev/null +++ b/Swiftgram/SGProUI/BUILD @@ -0,0 +1,41 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + + +swift_library( + name = "SGProUI", + module_name = "SGProUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGKeychainBackupManager:SGKeychainBackupManager", + "//Swiftgram/SGItemListUI:SGItemListUI", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGStrings:SGStrings", + "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGSwiftUI:SGSwiftUI", + # + "//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/SGProUI/Sources/MessageFilterController.swift b/Swiftgram/SGProUI/Sources/MessageFilterController.swift new file mode 100644 index 0000000000..eb36ac2ef5 --- /dev/null +++ b/Swiftgram/SGProUI/Sources/MessageFilterController.swift @@ -0,0 +1,181 @@ +import Foundation +import SwiftUI +import SGSwiftUI +import SGStrings +import SGSimpleSettings +import LegacyUI +import Display +import TelegramPresentationData + +@available(iOS 13.0, *) +struct MessageFilterKeywordInputFieldModifier: ViewModifier { + @Binding var newKeyword: String + let onAdd: () -> Void + + func body(content: Content) -> some View { + if #available(iOS 15.0, *) { + content + .submitLabel(.return) + .submitScope(false) // TODO(swiftgram): Keyboard still closing + .interactiveDismissDisabled() + .onSubmit { + onAdd() + } + } else { + content + } + } +} + + +@available(iOS 13.0, *) +struct MessageFilterKeywordInputView: View { + @Environment(\.lang) var lang: String + @Binding var newKeyword: String + let onAdd: () -> Void + + var body: some View { + HStack { + TextField("MessageFilter.InputPlaceholder".i18n(lang), text: $newKeyword) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .keyboardType(.default) + .modifier(MessageFilterKeywordInputFieldModifier(newKeyword: $newKeyword, onAdd: onAdd)) + + + Button(action: onAdd) { + Image(systemName: "plus.circle.fill") + .foregroundColor(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor) + .imageScale(.large) + } + .disabled(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .buttonStyle(PlainButtonStyle()) + } + } +} + +@available(iOS 13.0, *) +struct MessageFilterView: View { + weak var wrapperController: LegacyController? + @Environment(\.lang) var lang: String + + @State private var newKeyword: String = "" + @State private var keywords: [String] { + didSet { + SGSimpleSettings.shared.messageFilterKeywords = keywords + } + } + + init(wrapperController: LegacyController?) { + self.wrapperController = wrapperController + _keywords = State(initialValue: SGSimpleSettings.shared.messageFilterKeywords) + } + + var bodyContent: some View { + List { + Section { + // Icon and title + VStack(spacing: 8) { + Image(systemName: "nosign.app.fill") + .font(.system(size: 50)) + .foregroundColor(.secondary) + + Text("MessageFilter.Title".i18n(lang)) + .font(.title) + .bold() + + Text("MessageFilter.SubTitle".i18n(lang)) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .listRowInsets(EdgeInsets()) + + } + + Section { + MessageFilterKeywordInputView(newKeyword: $newKeyword, onAdd: addKeyword) + } + + Section(header: Text("MessageFilter.Keywords.Title".i18n(lang))) { + ForEach(keywords.reversed(), id: \.self) { keyword in + Text(keyword) + } + .onDelete { indexSet in + let originalIndices = IndexSet( + indexSet.map { keywords.count - 1 - $0 } + ) + deleteKeywords(at: originalIndices) + } + } + } + .tgNavigationBackButton(wrapperController: wrapperController) + } + + var body: some View { + NavigationView { + if #available(iOS 14.0, *) { + bodyContent + .toolbar { + EditButton() + } + } else { + bodyContent + } + } + } + + private func addKeyword() { + let trimmedKeyword = newKeyword.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedKeyword.isEmpty else { return } + + let keywordExists = keywords.contains { + $0 == trimmedKeyword + } + + guard !keywordExists else { + return + } + + withAnimation { + keywords.append(trimmedKeyword) + } + newKeyword = "" + + } + + private func deleteKeywords(at offsets: IndexSet) { + withAnimation { + keywords.remove(atOffsets: offsets) + } + } +} + +@available(iOS 13.0, *) +public func sgMessageFilterController(presentationData: PresentationData? = nil) -> ViewController { + let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme) + let strings = presentationData?.strings ?? defaultPresentationStrings + + let legacyController = LegacySwiftUIController( + presentation: .navigation, + theme: theme, + strings: strings + ) + // Status bar color will break if theme changed + legacyController.statusBar.statusBarStyle = theme.rootController + .statusBarStyle.style + legacyController.displayNavigationBar = false + let swiftUIView = SGSwiftUIView( + legacyController: legacyController, + content: { + MessageFilterView(wrapperController: legacyController) + } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) + + return legacyController +} diff --git a/Swiftgram/SGProUI/Sources/SGProUI.swift b/Swiftgram/SGProUI/Sources/SGProUI.swift new file mode 100644 index 0000000000..a10f7d8eac --- /dev/null +++ b/Swiftgram/SGProUI/Sources/SGProUI.swift @@ -0,0 +1,186 @@ +import Foundation +import UniformTypeIdentifiers +import SGItemListUI +import UndoUI +import AccountContext +import Display +import TelegramCore +import Postbox +import ItemListUI +import SwiftSignalKit +import TelegramPresentationData +import PresentationDataUtils +import TelegramUIPreferences + +// Optional +import SGSimpleSettings +import SGLogging + + +private enum SGProControllerSection: Int32, SGItemListSection { + case base + case notifications + case footer +} + +private enum SGProDisclosureLink: String { + case sessionBackupManager + case messageFilter +} + +private enum SGProToggles: String { + case inputToolbar +} + +private enum SGProOneFromManySetting: String { + case pinnedMessageNotifications + case mentionsAndRepliesNotifications +} + +private enum SGProAction { + case resetIAP +} + +private typealias SGProControllerEntry = SGItemListUIEntry + +private func SGProControllerEntries(presentationData: PresentationData) -> [SGProControllerEntry] { + var entries: [SGProControllerEntry] = [] + let lang = presentationData.strings.baseLanguageCode + + let id = SGItemListCounter() + + entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "SessionBackup.Title".i18n(lang))) + entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "MessageFilter.Title".i18n(lang))) + entries.append(.toggle(id: id.count, section: .base, settingName: .inputToolbar, value: SGSimpleSettings.shared.inputToolbar, text: "InputToolbar.Title".i18n(lang), enabled: true)) + + entries.append(.header(id: id.count, section: .notifications, text: presentationData.strings.Notifications_Title.uppercased(), badge: nil)) + entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .pinnedMessageNotifications, text: "Notifications.PinnedMessages.Title".i18n(lang), value: "Notifications.PinnedMessages.value.\(SGSimpleSettings.shared.pinnedMessageNotifications)".i18n(lang), enabled: true)) + entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .mentionsAndRepliesNotifications, text: "Notifications.MentionsAndReplies.Title".i18n(lang), value: "Notifications.MentionsAndReplies.value.\(SGSimpleSettings.shared.mentionsAndRepliesNotifications)".i18n(lang), enabled: true)) + + #if DEBUG + entries.append(.action(id: id.count, section: .footer, actionType: .resetIAP, text: "Reset Pro", kind: .destructive)) + #endif + + return entries +} + +public 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 sgProController(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 .inputToolbar: + SGSimpleSettings.shared.inputToolbar = value + } + }, setOneFromManyValue: { setting in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let lang = presentationData.strings.baseLanguageCode + let actionSheet = ActionSheetController(presentationData: presentationData) + var items: [ActionSheetItem] = [] + + switch (setting) { + case .pinnedMessageNotifications: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.pinnedMessageNotifications = value + SGSimpleSettings.shared.synchronizeShared() + simplePromise.set(true) + } + + for value in SGSimpleSettings.PinnedMessageNotificationsSettings.allCases { + items.append(ActionSheetButtonItem(title: "Notifications.PinnedMessages.value.\(value.rawValue)".i18n(lang), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } + case .mentionsAndRepliesNotifications: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.mentionsAndRepliesNotifications = value + SGSimpleSettings.shared.synchronizeShared() + simplePromise.set(true) + } + + for value in SGSimpleSettings.MentionsAndRepliesNotificationsSettings.allCases { + items.append(ActionSheetButtonItem(title: "Notifications.MentionsAndReplies.value.\(value.rawValue)".i18n(lang), 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 + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch (link) { + case .sessionBackupManager: + if #available(iOS 13.0, *) { + pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData)) + } else { + presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil) + } + case .messageFilter: + if #available(iOS 13.0, *) { + pushControllerImpl?(sgMessageFilterController(presentationData: presentationData)) + } else { + presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil) + } + } + }, action: { action in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + switch action { + case .resetIAP: + let updateSettingsSignal = updateSGStatusInteractively(accountManager: context.sharedContext.accountManager, { status in + var status = status + status.status = SGStatus.default.status + SGSimpleSettings.shared.primaryUserId = "" + return status + }) + let _ = (updateSettingsSignal |> deliverOnMainQueue).start(next: { + presentControllerImpl?(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Status reset completed. You can now restore purchases.", timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ), + nil) + }) + } + }) + + let signal = combineLatest(context.sharedContext.presentationData, simplePromise.get()) + |> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in + + let entries = SGProControllerEntries(presentationData: presentationData) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram Pro"), 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/SGProUI/Sources/SessionBackupController.swift b/Swiftgram/SGProUI/Sources/SessionBackupController.swift new file mode 100644 index 0000000000..dd42ff8ac8 --- /dev/null +++ b/Swiftgram/SGProUI/Sources/SessionBackupController.swift @@ -0,0 +1,520 @@ +import Foundation +import UndoUI +import AccountContext +import TelegramCore +import Postbox +import Display +import SwiftSignalKit +import TelegramPresentationData +import PresentationDataUtils +import SGSimpleSettings +import SGLogging +import SGKeychainBackupManager + +struct SessionBackup: Codable { + var name: String? = nil + var date: Date = Date() + let accountRecord: AccountRecord + + var peerIdInternal: Int64 { + var userId: Int64 = 0 + for attribute in accountRecord.attributes { + if case let .backupData(backupData) = attribute, let backupPeerID = backupData.data?.peerId { + userId = backupPeerID + break + } + } + return userId + } + + var userId: Int64 { + return PeerId(peerIdInternal).id._internalGetInt64Value() + } +} + +import SwiftUI +import SGSwiftUI +import LegacyUI +import SGStrings + + +@available(iOS 13.0, *) +struct SessionBackupRow: View { + @Environment(\.lang) var lang: String + let backup: SessionBackup + let isLoggedIn: Bool + + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + var formattedDate: String { + if #available(iOS 15.0, *) { + return backup.date.formatted(date: .abbreviated, time: .shortened) + } else { + return dateFormatter.string(from: backup.date) + } + } + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(backup.name ?? String(backup.userId)) + .font(.body) + + Text("ID: \(backup.userId)") + .font(.subheadline) + .foregroundColor(.secondary) + + Text("SessionBackup.LastBackupAt".i18n(lang, args: formattedDate)) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + Text((isLoggedIn ? "SessionBackup.LoggedIn" : "SessionBackup.LoggedOut").i18n(lang)) + .font(.caption) + .foregroundColor(isLoggedIn ? .white : .secondary) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(isLoggedIn ? Color.accentColor : Color.secondary.opacity(0.1)) + .cornerRadius(4) + } + .padding(.vertical, 4) + } +} + + +@available(iOS 13.0, *) +struct BorderedButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.accentColor, lineWidth: 1) + ) + .opacity(configuration.isPressed ? 0.7 : 1.0) + } +} + +@available(iOS 13.0, *) +struct SessionBackupManagerView: View { + @Environment(\.lang) var lang: String + weak var wrapperController: LegacyController? + let context: AccountContext + + @State private var sessions: [SessionBackup] = [] + @State private var loggedInPeerIDs: [Int64] = [] + @State private var loggedInAccountsDisposable: Disposable? = nil + + private func performBackup() { + let controller = OverlayStatusController(theme: context.sharedContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil)) + + let signal = context.sharedContext.accountManager.accountRecords() + |> take(1) + |> deliverOnMainQueue + + let signal2 = context.sharedContext.activeAccountsWithInfo + |> take(1) + |> deliverOnMainQueue + + wrapperController?.present(controller, in: .window(.root), with: nil) + + Task { + if let result = try? await combineLatest(signal, signal2).awaitable() { + let (view, accountsWithInfo) = result + backupSessionsFromView(view, accountsWithInfo: accountsWithInfo.1) + withAnimation { + sessions = getBackedSessions() + } + controller.dismiss() + } + } + + } + + private func performRestore() { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + + let _ = (context.sharedContext.accountManager.accountRecords() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] view in + + let backupSessions = getBackedSessions() + var restoredSessions: Int64 = 0 + + func importNextBackup(index: Int) { + // Check if we're done + if index >= backupSessions.count { + // All done, update UI + withAnimation { + sessions = getBackedSessions() + } + controller?.dismiss() + wrapperController?.present( + okUndoController("SessionBackup.RestoreOK".i18n(lang, args: "\(restoredSessions)"), presentationData), + in: .current + ) + return + } + + let backup = backupSessions[index] + + // Check for existing record + let existingRecord = view.records.first { record in + var userId: Int64 = 0 + for attribute in record.attributes { + if case let .backupData(backupData) = attribute { + userId = backupData.data?.peerId ?? 0 + } + } + return userId == backup.peerIdInternal + } + + if existingRecord != nil { + SGLogger.shared.log("SessionBackup", "Record \(backup.userId) already exists, skipping") + importNextBackup(index: index + 1) + return + } + + var importAttributes = backup.accountRecord.attributes + importAttributes.removeAll { attribute in + if case .sortOrder = attribute { + return true + } + return false + } + + let importBackupSignal = context.sharedContext.accountManager.transaction { transaction -> Void in + let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in + for attribute in record.attributes { + if case let .sortOrder(sortOrder) = attribute { + return sortOrder.order + } + } + return 0 + }).max() ?? 0) + 1 + importAttributes.append(.sortOrder(AccountSortOrderAttribute(order: nextSortOrder))) + let accountRecordId = transaction.createRecord(importAttributes) + SGLogger.shared.log("SessionBackup", "Imported record \(accountRecordId) for \(backup.userId)") + restoredSessions += 1 + } + |> deliverOnMainQueue + + let _ = importBackupSignal.start(completed: { + importNextBackup(index: index + 1) + }) + } + + // Start the import chain + importNextBackup(index: 0) + }) + + wrapperController?.present(controller, in: .window(.root), with: nil) + } + + private func performDeleteAll() { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let controller = textAlertController(context: context, title: "SessionBackup.DeleteAll.Title".i18n(lang), text: "SessionBackup.DeleteAll.Text".i18n(lang), actions: [ + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + wrapperController?.present(controller, in: .window(.root), with: nil) + do { + try KeychainBackupManager.shared.deleteAllSessions() + withAnimation { + sessions = getBackedSessions() + } + controller.dismiss() + } catch let e { + SGLogger.shared.log("SessionBackup", "Error deleting all sessions: \(e)") + } + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}) + ]) + + wrapperController?.present(controller, in: .window(.root), with: nil) + } + + private func performDelete(_ session: SessionBackup) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let controller = textAlertController(context: context, title: "SessionBackup.DeleteSingle.Title".i18n(lang), text: "SessionBackup.DeleteSingle.Text".i18n(lang, args: "\(session.name ?? "\(session.userId)")"), actions: [ + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + wrapperController?.present(controller, in: .window(.root), with: nil) + do { + try KeychainBackupManager.shared.deleteSession(for: "\(session.peerIdInternal)") + withAnimation { + sessions = getBackedSessions() + } + controller.dismiss() + } catch let e { + SGLogger.shared.log("SessionBackup", "Error deleting session: \(e)") + } + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}) + ]) + + wrapperController?.present(controller, in: .window(.root), with: nil) + } + + + private func performRemoveSessionFromApp(session: SessionBackup) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let controller = textAlertController(context: context, title: "SessionBackup.RemoveFromApp.Title".i18n(lang), text: "SessionBackup.RemoveFromApp.Text".i18n(lang, args: "\(session.name ?? "\(session.userId)")"), actions: [ + TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + wrapperController?.present(controller, in: .window(.root), with: nil) + + let signal = context.sharedContext.accountManager.accountRecords() + |> take(1) + |> deliverOnMainQueue + + let _ = signal.start(next: { [weak controller] view in + + // Find record to delete + let accountRecord = view.records.first { record in + var userId: Int64 = 0 + for attribute in record.attributes { + if case let .backupData(backupData) = attribute { + userId = backupData.data?.peerId ?? 0 + } + } + return userId == session.peerIdInternal + } + + if let record = accountRecord { + let deleteSignal = context.sharedContext.accountManager.transaction { transaction -> Void in + transaction.updateRecord(record.id, { _ in return nil}) + } + |> deliverOnMainQueue + + let _ = deleteSignal.start(next: { + withAnimation { + sessions = getBackedSessions() + } + controller?.dismiss() + }) + } else { + controller?.dismiss() + } + }) + + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}) + ]) + + wrapperController?.present(controller, in: .window(.root), with: nil) + } + + + var body: some View { + List { + Section() { + Button(action: performBackup) { + HStack { + Image(systemName: "key.fill") + .frame(width: 30) + Text("SessionBackup.Actions.Backup".i18n(lang)) + Spacer() + } + } + + Button(action: performRestore) { + HStack { + Image(systemName: "arrow.2.circlepath") + .frame(width: 30) + Text("SessionBackup.Actions.Restore".i18n(lang)) + Spacer() + } + } + + Button(action: performDeleteAll) { + HStack { + Image(systemName: "trash") + .frame(width: 30) + Text("SessionBackup.Actions.DeleteAll".i18n(lang)) + } + } + .foregroundColor(.red) + + } + + Text("SessionBackup.Notice".i18n(lang)) + .font(.caption) + .foregroundColor(.secondary) + + Section(header: Text("SessionBackup.Sessions.Title".i18n(lang))) { + ForEach(sessions, id: \.peerIdInternal) { session in + SessionBackupRow( + backup: session, + isLoggedIn: loggedInPeerIDs.contains(session.peerIdInternal) + ) + .contextMenu { + Button(action: { + performDelete(session) + }, label: { + HStack(spacing: 4) { + Text("SessionBackup.Actions.DeleteOne".i18n(lang)) + Image(systemName: "trash") + } + }) + Button(action: { + performRemoveSessionFromApp(session: session) + }, label: { + + HStack(spacing: 4) { + Text("SessionBackup.Actions.RemoveFromApp".i18n(lang)) + Image(systemName: "trash") + } + }) + } + } +// .onDelete { indexSet in +// performDelete(indexSet) +// } + } + } + .onAppear { + withAnimation { + sessions = getBackedSessions() + } + + let accountsSignal = context.sharedContext.accountManager.accountRecords() + |> deliverOnMainQueue + + loggedInAccountsDisposable = accountsSignal.start(next: { view in + var result: [Int64] = [] + for record in view.records { + var isLoggedOut: Bool = false + var userId: Int64 = 0 + for attribute in record.attributes { + if case .loggedOut = attribute { + isLoggedOut = true + } else if case let .backupData(backupData) = attribute { + userId = backupData.data?.peerId ?? 0 + } + } + + if !isLoggedOut && userId != 0 { + result.append(userId) + } + } + + SGLogger.shared.log("SessionBackup", "Logged in accounts: \(result)") + if loggedInPeerIDs != result { + SGLogger.shared.log("SessionBackup", "Updating logged in accounts: \(result)") + loggedInPeerIDs = result + } + }) + + } + .onDisappear { + loggedInAccountsDisposable?.dispose() + } + } + +} + + +func getBackedSessions() -> [SessionBackup] { + var sessions: [SessionBackup] = [] + do { + let backupSessionsData = try KeychainBackupManager.shared.getAllSessons() + for sessionBackupData in backupSessionsData { + do { + let backup = try JSONDecoder().decode(SessionBackup.self, from: sessionBackupData) + sessions.append(backup) + } catch let e { + SGLogger.shared.log("SessionBackup", "IMPORT ERROR: \(e)") + } + } + } catch let e { + SGLogger.shared.log("SessionBackup", "Error getting all sessions: \(e)") + } + return sessions +} + + +func backupSessionsFromView(_ view: AccountRecordsView, accountsWithInfo: [AccountWithInfo] = []) { + var recordsToBackup: [Int64: AccountRecord] = [:] + for record in view.records { + var sortOrder: Int32 = 0 + var isLoggedOut: Bool = false + var isTestingEnvironment: Bool = false + var peerId: Int64 = 0 + for attribute in record.attributes { + if case let .sortOrder(value) = attribute { + sortOrder = value.order + } else if case .loggedOut = attribute { + isLoggedOut = true + } else if case let .environment(environment) = attribute, case .test = environment.environment { + isTestingEnvironment = true + } else if case let .backupData(backupData) = attribute { + peerId = backupData.data?.peerId ?? 0 + } + } + let _ = sortOrder + let _ = isTestingEnvironment + + if !isLoggedOut && peerId != 0 { + recordsToBackup[peerId] = record + } + } + + for (peerId, record) in recordsToBackup { + var backupName: String? = nil + if let accountWithInfo = accountsWithInfo.first(where: { $0.peer.id == PeerId(peerId) }) { + if let user = accountWithInfo.peer as? TelegramUser { + if let username = user.username { + backupName = "@\(username)" + } else { + backupName = user.nameOrPhone + } + } + } + let backup = SessionBackup(name: backupName, accountRecord: record) + do { + let data = try JSONEncoder().encode(backup) + try KeychainBackupManager.shared.saveSession(id: "\(backup.peerIdInternal)", data) + } catch let e { + SGLogger.shared.log("SessionBackup", "BACKUP ERROR: \(e)") + } + } +} + + +@available(iOS 13.0, *) +public func sgSessionBackupManagerController(context: AccountContext, presentationData: PresentationData? = nil) -> ViewController { + let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme) + let strings = presentationData?.strings ?? defaultPresentationStrings + + let legacyController = LegacySwiftUIController( + presentation: .navigation, + theme: theme, + strings: strings + ) + legacyController.statusBar.statusBarStyle = theme.rootController + .statusBarStyle.style + legacyController.title = "SessionBackup.Title".i18n(strings.baseLanguageCode) + + let swiftUIView = SGSwiftUIView( + legacyController: legacyController, + manageSafeArea: true, + content: { + SessionBackupManagerView(wrapperController: legacyController, context: context) + } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) + + return legacyController +} diff --git a/Swiftgram/SGRegDate/BUILD b/Swiftgram/SGRegDate/BUILD new file mode 100644 index 0000000000..ff5f233e30 --- /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 0000000000..0be0019683 --- /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 0000000000..008f82658d --- /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 0000000000..a972377e8b --- /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 0000000000..979d84f32e --- /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 0000000000..19dfa3da27 --- /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 0000000000..e0d37a3c51 --- /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 0000000000..148a22836f --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist @@ -0,0 +1,47 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + FooterText + Reset.Notice + Title + Reset.Title + + + Type + PSToggleSwitchSpecifier + Title + Reset.Toggle + Key + sg_db_reset + DefaultValue + + + + Type + PSGroupSpecifier + FooterText + HardReset.Notice + Title + HardReset.Title + + + Type + PSToggleSwitchSpecifier + Title + HardReset.Toggle + Key + sg_db_hard_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 0000000000..e40aa8c250 --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/en.lproj/Root.strings @@ -0,0 +1,8 @@ +/* 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 Metadata"; +"Reset.Notice" = "Use in case you're stuck and can't open the app. This WILL NOT log out your accounts, but all secret chats will be lost."; +"HardReset.Title" = ""; +"HardReset.Toggle" = "Reset All"; +"HardReset.Notice" = "Clears metadata, cached messages and media for all accounts. This should not log out your accounts, but proceed at YOUR OWN RISK. All secret chats will be lost."; \ No newline at end of file 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 0000000000..a0f39d27b4 --- /dev/null +++ b/Swiftgram/SGSettingsBundle/Settings.bundle/ru.lproj/Root.strings @@ -0,0 +1,6 @@ +"Reset.Title" = "РЕШЕНИЕ ПРОБЛЕМ"; +"Reset.Toggle" = "Сбросить Метаданные"; +"Reset.Notice" = "Используйте, если приложение вылетает или не загружается. Эта опция НЕ СБРАСЫВАЕТ ваши аккаунты, но удалит все секретные чаты."; +"HardReset.Title" = ""; +"HardReset.Toggle" = "Сбросить Всё"; +"HardReset.Notice" = "Сбрасывает метаданные, кэшированные сообщения и медиа для всех аккаунтов. Эта опция не должна разлогинить ваши аккаунты, но используйте её на СВОЙ СТРАХ И РИСК. Все секретные чаты удалятся."; \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/BUILD b/Swiftgram/SGSettingsUI/BUILD new file mode 100644 index 0000000000..dc1613e781 --- /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 0000000000..73c00596a7 --- /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 0000000000..526cf46d7c --- /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 0000000000..ed4efd9629 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 0000000000..6fb419fc51 --- /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 0000000000..30789ecb77 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf @@ -0,0 +1,81 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.964844 cm +0.000000 0.000000 0.000000 scn +15.076375 10.766671 m +15.473662 11.399487 14.937258 12.204764 14.200223 12.081993 c +9.059459 11.225675 l +8.855769 11.191745 8.670359 11.348825 8.670359 11.555322 c +8.670359 18.524288 l +8.670359 19.289572 7.652856 19.554642 7.279467 18.886631 c +1.036950 7.718488 l +0.658048 7.040615 1.293577 6.244993 2.038416 6.464749 c +9.378864 8.630468 l +9.637225 8.706696 9.814250 8.373775 9.606588 8.202201 c +6.918006 5.980853 l +6.462659 5.604639 6.199009 5.044809 6.199009 4.454151 c +6.199009 -0.793964 l +6.199009 -1.539309 7.174314 -1.820084 7.570620 -1.188831 c +15.076375 10.766671 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 702 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000792 00000 n +0000000814 00000 n +0000000987 00000 n +0000001061 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1120 +%%EOF \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json new file mode 100644 index 0000000000..7506e639eb --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "SwiftgramPro.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } + } + \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/SwiftgramPro.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/SwiftgramPro.pdf new file mode 100644 index 0000000000..fb4264fd56 Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/SwiftgramPro.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 0000000000..1bf20b6bc8 --- /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 0000000000..6abd681bf6 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf @@ -0,0 +1,242 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop } +endstream +endobj + +2 0 obj + 339 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 1 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +/Pattern cs +/P1 scn +0.000000 320.853333 m +0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c +24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c +90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c +320.853333 512.000000 l +387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c +469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c +512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c +512.000000 191.146667 l +512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c +487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c +421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c +191.146667 0.000000 l +124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c +42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c +0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c +0.000000 320.853333 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 119.500000 103.400391 cm +1.000000 1.000000 1.000000 scn +256.015533 182.826599 m +262.761963 193.572601 253.653152 207.247192 241.137390 205.162399 c +153.840836 190.621048 l +150.381927 190.044891 147.233429 192.712296 147.233429 196.218872 c +147.233429 314.560455 l +147.233429 327.555908 129.954987 332.057098 123.614365 320.713440 c +17.608702 131.064743 l +11.174477 119.553635 21.966566 106.042999 34.614845 109.774734 c +159.264740 146.551285 l +163.652023 147.845703 166.658112 142.192291 163.131760 139.278763 c +117.476318 101.557587 l +109.743965 95.169006 105.266861 85.662384 105.266861 75.632263 c +105.266861 -13.487152 l +105.266861 -26.143982 121.828712 -30.911926 128.558456 -20.192505 c +256.015533 182.826599 l +h +f* +n +Q + +endstream +endobj + +4 0 obj + 1771 +endobj + +5 0 obj + << /Length 6 0 R + /Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ] + /Domain [ 0.000000 1.000000 ] + /FunctionType 4 + >> +stream +{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop } +endstream +endobj + +6 0 obj + 339 +endobj + +7 0 obj + << /Type /XObject + /Length 8 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ] + /Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ] + /ColorSpace /DeviceRGB + /Function 5 0 R + /Domain [ 0.000000 1.000000 ] + /ShadingType 2 + /Extend [ true true ] + >> + /PatternType 2 + /Type /Pattern + >> >> >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +/Pattern cs +/P1 scn +0.000000 320.853333 m +0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c +24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c +90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c +320.853333 512.000000 l +387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c +469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c +512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c +512.000000 191.146667 l +512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c +487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c +421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c +191.146667 0.000000 l +124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c +42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c +0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c +0.000000 320.853333 l +h +f +n +Q + +endstream +endobj + +8 0 obj + 1006 +endobj + +9 0 obj + << /XObject << /X1 3 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 7 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +10 0 obj + << /Length 11 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +11 0 obj + 46 +endobj + +12 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 512.000000 512.000000 ] + /Resources 9 0 R + /Contents 10 0 R + /Parent 13 0 R + >> +endobj + +13 0 obj + << /Kids [ 12 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +14 0 obj + << /Pages 13 0 R + /Type /Catalog + >> +endobj + +xref +0 15 +0000000000 65535 f +0000000010 00000 n +0000000533 00000 n +0000000555 00000 n +0000003331 00000 n +0000003354 00000 n +0000003877 00000 n +0000003899 00000 n +0000005910 00000 n +0000005933 00000 n +0000006231 00000 n +0000006335 00000 n +0000006357 00000 n +0000006535 00000 n +0000006611 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 14 0 R + /Size 15 +>> +startxref +6672 +%%EOF \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift new file mode 100644 index 0000000000..80e0c00467 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift @@ -0,0 +1,681 @@ +// 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 disableGalleryCameraPreview + 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 secondsInMessages + case hideChannelBottomButton + case confirmCalls + case swipeForVideoPIP +} + +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: .swipeForVideoPIP, value: SGSimpleSettings.shared.videoPIPSwipeDirection == SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue, text: i18n("Settings.swipeForVideoPIP", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.swipeForVideoPIP.Notice", lang))) + entries.append(.toggle(id: id.count, section: .other, settingName: .hideChannelBottomButton, value: !SGSimpleSettings.shared.hideChannelBottomButton, text: i18n("Settings.showChannelBottomButton", 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: .secondsInMessages, value: SGSimpleSettings.shared.secondsInMessages, text: i18n("Settings.secondsInMessages", lang), enabled: true)) + 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: .disableGalleryCameraPreview, value: !SGSimpleSettings.shared.disableGalleryCameraPreview, text: i18n("Settings.GalleryCameraPreview", lang), enabled: !SGSimpleSettings.shared.disableGalleryCamera)) + 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(.notice(id: id.count, section: .other, text: i18n("Settings.DownloadsBoost.Notice", lang))) + 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 + simplePromise.set(true) + case .disableGalleryCameraPreview: + SGSimpleSettings.shared.disableGalleryCameraPreview = !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 .secondsInMessages: + SGSimpleSettings.shared.secondsInMessages = value + case .confirmCalls: + SGSimpleSettings.shared.confirmCalls = value + case .swipeForVideoPIP: + SGSimpleSettings.shared.videoPIPSwipeDirection = value ? SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue : SGSimpleSettings.VideoPIPSwipeDirection.none.rawValue + } + }, 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 0000000000..8097e4c906 --- /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 0000000000..7868b0db3a --- /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 0000000000..b43e660d31 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/BUILD @@ -0,0 +1,18 @@ +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 = [ + "//Swiftgram/SGAppGroupIdentifier:SGAppGroupIdentifier", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift b/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift new file mode 100644 index 0000000000..b0d073605d --- /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 0000000000..3ea2436c6f --- /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 0000000000..d779d47c07 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift @@ -0,0 +1,520 @@ +import Foundation +import SGAppGroupIdentifier + +let APP_GROUP_IDENTIFIER = sgAppGroupIdentifier() + +public class SGSimpleSettings { + + public static let shared = SGSimpleSettings() + + private init() { + setDefaultValues() + preCacheValues() + } + + private func setDefaultValues() { + UserDefaults.standard.register(defaults: SGSimpleSettings.defaultValues) + // Just in case group defaults will be nil + UserDefaults.standard.register(defaults: SGSimpleSettings.groupDefaultValues) + if let groupUserDefaults = UserDefaults(suiteName: APP_GROUP_IDENTIFIER) { + groupUserDefaults.register(defaults: SGSimpleSettings.groupDefaultValues) + } + } + + 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 }, + { let _ = self.inputToolbar }, + { let _ = self.dismissedSGSuggestions } + ] + + tasks.forEach { task in + DispatchQueue.global(qos: .background).async(/*group: dispatchGroup*/) { + task() + } + } + + // dispatchGroup.notify(queue: DispatchQueue.main) {} + } + + public func synchronizeShared() { + if let groupUserDefaults = UserDefaults(suiteName: APP_GROUP_IDENTIFIER) { + groupUserDefaults.synchronize() + } + } + + 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 disableGalleryCameraPreview + 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 secondsInMessages + case hideChannelBottomButton + case forceSystemSharing + case confirmCalls + case videoPIPSwipeDirection + case legacyNotificationsFix + case messageFilterKeywords + case inputToolbar + case pinnedMessageNotifications + case mentionsAndRepliesNotifications + case primaryUserId + case status + case dismissedSGSuggestions + case duckyAppIconAvailable + } + + 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 enum VideoPIPSwipeDirection: String, CaseIterable { + case up + case down + case none + } + + public enum PinnedMessageNotificationsSettings: String, CaseIterable { + case `default` + case silenced + case disabled + } + + public enum MentionsAndRepliesNotificationsSettings: String, CaseIterable { + case `default` + case silenced + case disabled + } + + 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.disableGalleryCameraPreview.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.secondsInMessages.rawValue: false, + Keys.forceSystemSharing.rawValue: false, + Keys.confirmCalls.rawValue: true, + Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue, + Keys.messageFilterKeywords.rawValue: [], + Keys.inputToolbar.rawValue: false, + Keys.primaryUserId.rawValue: "", + Keys.dismissedSGSuggestions.rawValue: [], + Keys.duckyAppIconAvailable.rawValue: true + ] + + public static let groupDefaultValues: [String: Any] = [ + Keys.legacyNotificationsFix.rawValue: false, + Keys.pinnedMessageNotifications.rawValue: PinnedMessageNotificationsSettings.default.rawValue, + Keys.mentionsAndRepliesNotifications.rawValue: MentionsAndRepliesNotificationsSettings.default.rawValue, + Keys.status.rawValue: 1 + ] + + @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.disableGalleryCameraPreview.rawValue) + public var disableGalleryCameraPreview: 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.secondsInMessages.rawValue) + public var secondsInMessages: 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 + + @UserDefault(key: Keys.videoPIPSwipeDirection.rawValue) + public var videoPIPSwipeDirection: String + + @UserDefault(key: Keys.legacyNotificationsFix.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard) + public var legacyNotificationsFix: Bool + + @UserDefault(key: Keys.status.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard) + public var status: Int64 + + public var ephemeralStatus: Int64 = 1 + + @UserDefault(key: Keys.messageFilterKeywords.rawValue) + public var messageFilterKeywords: [String] + + @UserDefault(key: Keys.inputToolbar.rawValue) + public var inputToolbar: Bool + + @UserDefault(key: Keys.pinnedMessageNotifications.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard) + public var pinnedMessageNotifications: String + + @UserDefault(key: Keys.mentionsAndRepliesNotifications.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard) + public var mentionsAndRepliesNotifications: String + + @UserDefault(key: Keys.primaryUserId.rawValue) + public var primaryUserId: String + + @UserDefault(key: Keys.dismissedSGSuggestions.rawValue) + public var dismissedSGSuggestions: [String] + + @UserDefault(key: Keys.duckyAppIconAvailable.rawValue) + public var duckyAppIconAvailable: 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, fileSize: Int64?) -> Int64 { + let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost + // Increasing chunk size for small files make it worse in terms of overall download performance + let smallFileSizeThreshold = 1 * 1024 * 1024 // 1 MB + switch (currentDownloadSetting) { + case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue: + if let fileSize, fileSize <= smallFileSizeThreshold { + return `default` + } + return 512 * 1024 + case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue: + if let fileSize, fileSize <= smallFileSizeThreshold { + return `default` + } + 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 0000000000..48b0a37749 --- /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/SGStatus/BUILD b/Swiftgram/SGStatus/BUILD new file mode 100644 index 0000000000..acef413a33 --- /dev/null +++ b/Swiftgram/SGStatus/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGStatus", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGStatus/Sources/SGStatus.swift b/Swiftgram/SGStatus/Sources/SGStatus.swift new file mode 100644 index 0000000000..6bedd862a5 --- /dev/null +++ b/Swiftgram/SGStatus/Sources/SGStatus.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftSignalKit +import TelegramCore + +public struct SGStatus: Equatable, Codable { + public var status: Int64 + + public static var `default`: SGStatus { + return SGStatus(status: 1) + } + + public init(status: Int64) { + self.status = status + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.status = try container.decodeIfPresent(Int64.self, forKey: "status") ?? 1 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encodeIfPresent(self.status, forKey: "status") + } +} + +public func updateSGStatusInteractively(accountManager: AccountManager, _ f: @escaping (SGStatus) -> SGStatus) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.sgStatus, { entry in + let currentSettings: SGStatus + if let entry = entry?.get(SGStatus.self) { + currentSettings = entry + } else { + currentSettings = SGStatus.default + } + return SharedPreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/Swiftgram/SGStrings/BUILD b/Swiftgram/SGStrings/BUILD new file mode 100644 index 0000000000..dea968818a --- /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 0000000000..331586aa41 --- /dev/null +++ b/Swiftgram/SGStrings/Sources/LocalizationManager.swift @@ -0,0 +1,134 @@ +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) { + #if DEBUG + SGLogger.shared.log("Strings", "DEBUG ignoring locale download: \(locale)") + if ({ return true }()) { + return + } + #endif + 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 + + +public extension String { + func i18n(_ locale: String = SGFallbackLocale, args: CVarArg...) -> String { + return SGLocalizationManager.shared.localizedString(self, locale, args: args) + } +} diff --git a/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..5acfe970d5 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Verhoog die aantal parallelle verbindings en die grootte van lêerstukke. As jou netwerk nie die las kan hanteer nie, probeer verskillende opsies wat by jou verbinding pas."; +"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"; + +"Settings.CallConfirmation" = "Bel Bevestiging"; +"Settings.CallConfirmation.Notice" = "Swiftgram sal om jou bevestiging vra voordat 'n oproep gemaak word."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Maak 'n Oproep?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Maak 'n Video Oproep?"; + +"MutualContact.Label" = "ewige kontak"; + +"Settings.swipeForVideoPIP" = "Video PIP met Veeg"; +"Settings.swipeForVideoPIP.Notice" = "As geaktiveer, sal die veeg van die video dit in Prent-in-Prent modus oopmaak."; diff --git a/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..41f1168454 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "يزيد من عدد الاتصالات المتوازية وحجم أجزاء الملفات. إذا لم يتمكن شبكتك من تحمل الحمل، حاول خيارات مختلفة تناسب اتصالك."; +"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" = "إخفاء لوحة قاعدة القناة"; + +"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" = "جهة اتصال مشتركة"; + +"Settings.swipeForVideoPIP" = "فيديو PIP مع السحب"; +"Settings.swipeForVideoPIP.Notice" = "إذا تم تمكينه، سيفتح سحب الفيديو في وضع الصورة في الصورة."; diff --git a/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..a48c45fa92 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Augmenta el nombre de connexions paral·leles i la mida de les parts de fitxer. Si la teva xarxa no pot gestionar la càrrega, prova diferents opcions que s'adaptin a la teva connexió."; +"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"; + +"Settings.CallConfirmation" = "Confirmació de trucada"; +"Settings.CallConfirmation.Notice" = "Swiftgram et demanarà la teva confirmació abans de fer una trucada."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Vols fer una trucada?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Vols fer una videotrucada?"; + +"MutualContact.Label" = "contacte mutu"; + +"Settings.swipeForVideoPIP" = "Vídeo PIP amb desplaçament"; +"Settings.swipeForVideoPIP.Notice" = "Si està habilitat, desplaçar el vídeo l'obrirà en mode Imatge en Imatge."; diff --git a/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..05cf6ed482 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Zvyšuje počet paralelních připojení a velikost částí souboru. Pokud vaše síť nezvládá zátěž, vyzkoušejte různé možnosti, které vyhovují vašemu připojení."; +"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"; + +"Settings.CallConfirmation" = "Potvrzení hovoru"; +"Settings.CallConfirmation.Notice" = "Swiftgram požádá o vaši potvrzení před uskutečněním hovoru."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Uskutečnit hovor?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Uskutečnit video hovor?"; + +"MutualContact.Label" = "vzájemný kontakt"; + +"Settings.swipeForVideoPIP" = "Video PIP s přetahováním"; +"Settings.swipeForVideoPIP.Notice" = "Pokud je povoleno, poslání videa jej otevře v režimu Obraz v obraze."; diff --git a/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..cb0c4174db --- /dev/null +++ b/Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Øger antallet af parallelle forbindelser og størrelsen på filstykker. Hvis dit netværk ikke kan håndtere belastningen, kan du prøve forskellige muligheder, der passer til din forbindelse."; +"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"; + +"Settings.CallConfirmation" = "Opkaldsbekræftelse"; +"Settings.CallConfirmation.Notice" = "Swiftgram vil bede om din bekræftelse, før der foretages et opkald."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Foretage et opkald?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Foretage et videoopkald?"; + +"MutualContact.Label" = "fælles kontakt"; + +"Settings.swipeForVideoPIP" = "Video PIP med Swipe"; +"Settings.swipeForVideoPIP.Notice" = "Hvis aktiveret, vil sletning af video åbne den i billede-i-billede-tilstand."; diff --git a/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..726e485dcf --- /dev/null +++ b/Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Erhöht die Anzahl der parallelen Verbindungen und die Größe der Dateiabschnitte. Wenn Ihr Netzwerk die Last nicht bewältigen kann, versuchen Sie verschiedene Optionen, die zu Ihrer Verbindung passen."; +"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"; + +"Settings.CallConfirmation" = "Anrufbestätigung"; +"Settings.CallConfirmation.Notice" = "Swiftgram wird um Ihre Bestätigung bitten, bevor ein Anruf getätigt wird."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Einen Anruf tätigen?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Einen Videoanruf tätigen?"; + +"MutualContact.Label" = "gemeinsamer Kontakt"; + +"Settings.swipeForVideoPIP" = "Video PIP mit Wischen"; +"Settings.swipeForVideoPIP.Notice" = "Wenn aktiviert, öffnet das Wischen des Videos es im Bild-in-Bild-Modus."; diff --git a/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..010c2fd7ed --- /dev/null +++ b/Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Αυξάνει τον αριθμό των παράλληλων συνδέσεων και το μέγεθος των κομματιών αρχείου. Σε περίπτωση που το δίκτυό σας δεν μπορεί να διαχειριστεί το φορτίο, δοκιμάστε διαφορετικές επιλογές που ταιριάζουν στη σύνδεσή σας."; +"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" = "Απόκρυψη Καναλιού Κάτω Πάνελ"; + +"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" = "αμοιβαία επαφή"; + +"Settings.swipeForVideoPIP" = "Βίντεο PIP με Swipe"; +"Settings.swipeForVideoPIP.Notice" = "Αν είναι ενεργοποιημένο, το σ swipe video θα το ανοίξει σε λειτουργία Εικόνα μέσα στην Εικόνα."; diff --git a/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..f37c56ea3f --- /dev/null +++ b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "Increases number of parallel connections and size of file chunks. In case your network can't handle the load, try different options that suits your connection."; +"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."; +"Common.UpdateOS" = "iOS update required"; + +"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"; +"Settings.GalleryCameraPreview" = "Camera Preview 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" = "Standard emojis first"; +"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 tab first"; + +"Settings.forceBuiltInMic" = "Force Device Microphone"; +"Settings.forceBuiltInMic.Notice" = "If enabled, app will use only device microphone even if headphones are connected."; + +"Settings.showChannelBottomButton" = "Channel Bottom Panel"; + +"Settings.secondsInMessages" = "Seconds in Messages"; + +"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"; + +"Settings.swipeForVideoPIP" = "Video PIP with Swipe"; +"Settings.swipeForVideoPIP.Notice" = "If enabled, swiping video will open it in Picture-in-Picture mode."; + +"SessionBackup.Title" = "Accounts Backup"; +"SessionBackup.Sessions.Title" = "Sessions"; +"SessionBackup.Actions.Backup" = "Backup to Keychain"; +"SessionBackup.Actions.Restore" = "Restore from Keychain"; +"SessionBackup.Actions.DeleteAll" = "Delete Keychain Backup"; +"SessionBackup.Actions.DeleteOne" = "Delete from Backup"; +"SessionBackup.Actions.RemoveFromApp" = "Remove from App"; +"SessionBackup.LastBackupAt" = "Last Backup: %@"; +"SessionBackup.RestoreOK" = "OK. Sessions restored: %@"; +"SessionBackup.LoggedIn" = "Logged In"; +"SessionBackup.LoggedOut" = "Logged Out"; +"SessionBackup.DeleteAll.Title" = "Delete All Sessions?"; +"SessionBackup.DeleteAll.Text" = "All sessions will be removed from Keychain.\n\nAccounts will not be logged out from Swiftgram."; +"SessionBackup.DeleteSingle.Title" = "Delete 1 (one) Session?"; +"SessionBackup.DeleteSingle.Text" = "%@ session will be removed from Keychain.\n\nAccount will not be logged out from Swiftgram."; +"SessionBackup.RemoveFromApp.Title" = "Remove account from App?"; +"SessionBackup.RemoveFromApp.Text" = "%@ session WILL BE REMOVED from Swiftgram! Session will remain active, so you can restore it later."; +"SessionBackup.Notice" = "Sessions are encrypted and stored in the device Keychain. Sessions never leave your device.\n\nIMPORTANT: To restore sessions on a new device or after an OS reset, you MUST enable encrypted backups, otherwise Keychain won't be transfered.\n\nNOTE: Sessions may still be revoked by Telegram or from another device."; + +"MessageFilter.Title" = "Message Filter"; +"MessageFilter.SubTitle" = "Remove distractions and reduce visibility of messages containing keywords below.\nKeywords are case-sensitive."; +"MessageFilter.Keywords.Title" = "Keywords"; +"MessageFilter.InputPlaceholder" = "Enter keyword"; + +"InputToolbar.Title" = "Formatting Panel"; + +"Notifications.MentionsAndReplies.Title" = "@Mentions and Replies"; +"Notifications.MentionsAndReplies.value.default" = "Default"; +"Notifications.MentionsAndReplies.value.silenced" = "Muted"; +"Notifications.MentionsAndReplies.value.disabled" = "Disabled"; +"Notifications.PinnedMessages.Title" = "Pinned Messages"; +"Notifications.PinnedMessages.value.default" = "Default"; +"Notifications.PinnedMessages.value.silenced" = "Muted"; +"Notifications.PinnedMessages.value.disabled" = "Disabled"; + + +"PayWall.Text" = "Supercharged with Pro features"; + +"PayWall.SessionBackup.Title" = "Accounts Backup"; +"PayWall.SessionBackup.Notice" = "Log-in to accounts without code, even after reinstall. Secure storage with on-device Keychain."; +"PayWall.SessionBackup.Description" = "Changing device or deleting Swiftgram is no longer an issue. Restore all Sessions that are still Active on Telegram servers."; + +"PayWall.MessageFilter.Title" = "Message Filter"; +"PayWall.MessageFilter.Notice" = "Reduce visibility of SPAM, promotions and annoying messages."; +"PayWall.MessageFilter.Description" = "Create a list of keywords you don't want to see often and Swiftgram will reduce distractions."; + +"PayWall.Notifications.Title" = "Disable @mentions and replies"; +"PayWall.Notifications.Notice" = "Hide or mute non-important notifications."; +"PayWall.Notifications.Description" = "No more Pinned Messages or @mentions when you need some peace of mind."; + +"PayWall.InputToolbar.Title" = "Formatting Panel"; +"PayWall.InputToolbar.Notice" = "Bold, Italic, Links? Formatting with just a single tap."; +"PayWall.InputToolbar.Description" = "Apply and clear Formatting or insert new lines like a Pro."; + +"PayWall.AppIcons.Title" = "Unique App Icons"; +"PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen."; + +"PayWall.About.Title" = "About Swiftgram Pro"; +"PayWall.About.Notice" = "Free version of Swiftgram provides dozens of features and improvements over Telegram app. Innovating and keeping Swiftgram in sync with monthly Telegram updates is a huge effort that requires a lot of time and expensive hardware.\n\nSwiftgram is an open-source app that respects your privacy and doesn't bother you with ads. Subscribing to Swiftgram Pro you get access to exclusive features and support an independent developer."; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "Troubles with payment?"; +"PayWall.ProSupport.Contact" = "No worries!"; + +"PayWall.RestorePurchases" = "Restore Purchases"; +"PayWall.Terms" = "Terms of Service"; +"PayWall.Privacy" = "Privacy Policy"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "By subscribing to Swiftgram Pro you agree to the [Swiftgram Terms of Service](%1$@) and [Privacy Policy](%2$@)."; +"PayWall.Notice.Raw" = "By subscribing to Swiftgram Pro you agree to the Swiftgram Terms of Service and Privacy Policy."; + +"PayWall.Button.OpenPro" = "Use Pro features"; +"PayWall.Button.Purchasing" = "Purchasing..."; +"PayWall.Button.Restoring" = "Restoring Purchases..."; +"PayWall.Button.Validating" = "Validating Purchase..."; +"PayWall.Button.PaymentsUnavailable" = "Payments unavailable"; +"PayWall.Button.BuyInAppStore" = "Subscribe in App Store version"; +"PayWall.Button.Subscribe" = "Subscribe for %@ / month"; +"PayWall.Button.ContactingAppStore" = "Contacting App Store..."; + +"Paywall.Error.Title" = "Error"; +"PayWall.ValidationError" = "Validation Error"; +"PayWall.ValidationError.TryAgain" = "Something went wrong during purchase validation. No worries! Try to Restore Purchases a bit later."; +"PayWall.ValidationError.Expired" = "Your subscription expired. Subscribe again to regain access to Pro features."; diff --git a/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..4fcb6aee08 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Aumenta el número de conexiones paralelas y el tamaño de las partes del archivo. Si tu red no puede manejar la carga, prueba diferentes opciones que se adapten a tu conexión."; +"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"; + +"Settings.CallConfirmation" = "Confirmación de llamada"; +"Settings.CallConfirmation.Notice" = "Swiftgram pedirá tu confirmación antes de realizar una llamada."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "¿Hacer una llamada?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "¿Hacer una videollamada?"; + +"MutualContact.Label" = "contacto mutuo"; + +"Settings.swipeForVideoPIP" = "Video PIP con deslizamiento"; +"Settings.swipeForVideoPIP.Notice" = "Si está habilitado, deslizar el video lo abrirá en modo imagen en imagen."; diff --git a/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..1581d63536 --- /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 0000000000..3e7ea96fbf --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings @@ -0,0 +1,230 @@ +"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.Notice" = "Lisää samanaikaisten yhteyksien määrää ja tiedostopalojen kokoa. Jos verkkoasi ei pysty käsittelemään kuormitusta, kokeile erilaisia vaihtoehtoja, jotka sopivat yhteyteesi."; +"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."; +"Common.UpdateOS" = "iOS päivitys vaaditaan"; + +"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"; +"Settings.GalleryCameraPreview" = "Kameran esikatselu galleriassa"; +/* "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" = "Oletusemojit ensin"; +"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" = "Emojivälilehti ensin"; + +"Settings.forceBuiltInMic" = "Pakota laitteen mikrofoni"; +"Settings.forceBuiltInMic.Notice" = "Jos otettu käyttöön, sovellus käyttää vain laitteen mikrofonia, vaikka kuulokkeet olisivatkin liitettynä."; + +"Settings.showChannelBottomButton" = "Kanavan ala-paneeli"; + +"Settings.CallConfirmation" = "Puhelun vahvistus"; +"Settings.CallConfirmation.Notice" = "Swiftgram pyytää vahvistustasi ennen puhelun soittamista."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Soita puhelu?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Soita videopuhelu?"; + +"MutualContact.Label" = "yhteinen yhteys"; + +"Settings.swipeForVideoPIP" = "Video PIP pyyhkäisevällä toiminnolla"; +"Settings.swipeForVideoPIP.Notice" = "Jos se on käytössä, videon pyyhkäisy avaa sen kuvassa kuvassa -tilassa."; + +"SessionBackup.Title" = "Istunnon varmuuskopio"; +"SessionBackup.Sessions.Title" = "Istunnot"; +"SessionBackup.Actions.Backup" = "Varmenteet Keychainiin"; +"SessionBackup.Actions.Restore" = "Palauta Keychainista"; +"SessionBackup.Actions.DeleteAll" = "Poista Keychain-varmuuskopio"; +"SessionBackup.Actions.DeleteOne" = "Poista varmuuskopiosta"; +"SessionBackup.Actions.RemoveFromApp" = "Poista sovelluksesta"; +"SessionBackup.LastBackupAt" = "Viimeisin varmuuskopio: %@"; +"SessionBackup.RestoreOK" = "OK. Istunnot palautettu: %@"; +"SessionBackup.LoggedIn" = "Sisäänkirjautuneena"; +"SessionBackup.LoggedOut" = "Uloskirjautuneena"; +"SessionBackup.DeleteAll.Title" = "Poista kaikki istunnot?"; +"SessionBackup.DeleteAll.Text" = "Kaikki istunnot poistetaan Keychainista.\n\nTilejä ei kirjaudu ulos Swiftgramista."; +"SessionBackup.DeleteSingle.Title" = "Poista 1 (yksi) istunto?"; +"SessionBackup.DeleteSingle.Text" = "%@ istunto poistetaan Keychainista.\n\nTiliä ei kirjaudu ulos Swiftgramista."; +"SessionBackup.RemoveFromApp.Title" = "Poista tili sovelluksesta?"; +"SessionBackup.RemoveFromApp.Text" = "%@ istunto POISTETAAN Swiftgramista! Istunto pysyy aktiivisena, joten voit palauttaa sen myöhemmin."; +"SessionBackup.Notice" = "Istunnot on salattu ja tallennettu laitteen Keychain. Istunnot eivät koskaan jätä laitettasi.\n\nTÄRKEÄÄ: Voit palauttaa istunnot uudelle laitteelle tai käyttöjärjestelmän palautuksen jälkeen sinun TÄYTYY ottaa salatut varmuuskopiot käyttöön, muuten Keychain ei siirretä.\n\nHUOMAUTUS: Istunnot voidaan silti peruuttaa Telegramin tai toisen laitteen kautta."; + +"MessageFilter.Title" = "Viestisuoja"; +"MessageFilter.SubTitle" = "Poista häiriötekijät ja vähennä näkyvyyttä viesteistä, jotka sisältävät alla olevia avainsanoja.\nAvainsanat ovat erikoismerkkien suhteen herkkiä."; +"MessageFilter.Keywords.Title" = "Avainsanat"; +"MessageFilter.InputPlaceholder" = "Syötä avainsana"; + +"InputToolbar.Title" = "Muotoilupaneeli"; + +"Notifications.MentionsAndReplies.Title" = "@Maininnat ja vastaukset"; +"Notifications.MentionsAndReplies.value.default" = "Oletus"; +"Notifications.MentionsAndReplies.value.silenced" = "Mykistetty"; +"Notifications.MentionsAndReplies.value.disabled" = "Ei käytössä"; +"Notifications.PinnedMessages.Title" = "Kiinnitetyt viestit"; +"Notifications.PinnedMessages.value.default" = "Oletus"; +"Notifications.PinnedMessages.value.silenced" = "Mykistetty"; +"Notifications.PinnedMessages.value.disabled" = "Ei käytössä"; + + +"PayWall.Text" = "Tehostettu Pro-ominaisuuksilla"; + +"PayWall.SessionBackup.Title" = "Istunnon varmuuskopio"; +"PayWall.SessionBackup.Notice" = "Palauta istunnot salatusta paikallisesta Apple Keychain -varmuuskopiosta."; + +"PayWall.MessageFilter.Title" = "Viestisuodatin"; +"PayWall.MessageFilter.Notice" = "Vähennä roskapostin, mainosten ja ärsyttävien viestien näkyvyyttä."; + +"PayWall.Notifications.Title" = "Poista @maininnat ja vastaukset käytöstä"; +"PayWall.Notifications.Notice" = "Piilota tai mykistä ei-tärkeitä ilmoituksia."; + +"PayWall.InputToolbar.Title" = "Muotoilupaneeli"; +"PayWall.InputToolbar.Notice" = "Säästä aikaa valmistellessasi julkaisuja paneelilla juuri näppäimistösi yläpuolella."; + +"PayWall.AppIcons.Title" = "Ainutlaatuiset sovelluskuvakkeet"; +"PayWall.AppIcons.Notice" = "Mukauta Swiftgramin ulkoasu aloitusnäytölläsi."; + +"PayWall.About.Title" = "Tietoja Swiftgram Prosta"; +"PayWall.About.Notice" = "Swiftgramin ilmainen versio tarjoaa kymmeniä ominaisuuksia ja parannuksia Telegram-sovellukseen verrattuna. Innovointi ja Swiftgramin synkronointi kuukausittaisiin Telegram-päivityksiin vaatii valtavasti aikaa ja kallista laitteistoa.\n\nSwiftgram on avoimen lähdekoodin sovellus, joka kunnioittaa yksityisyyttäsi eikä vaivaa sinua mainoksilla. Tilatessasi Swiftgram Prota saat pääsyn eksklusiivisiin ominaisuuksiin ja tuet itsenäistä kehittäjää.\n\n- @Kylmakalle"; + +"PayWall.RestorePurchases" = "Palauta ostot"; +"PayWall.Terms" = "Käyttöehdot"; +"PayWall.Privacy" = "Tietosuojakäytäntö"; +"PayWall.TermsURL" = "https://swiftgram.app/ehtosuhteet"; +"PayWall.PrivacyURL" = "https://swiftgram.app/tietosuoja"; +"PayWall.Notice.Markdown" = "Tilatessasi Swiftgram Prota hyväksyt [Swiftgramin käyttöehdot](%1$@) ja [tietosuojakäytännön](%2$@)."; +"PayWall.Notice.Raw" = "Tilatessasi Swiftgram Prota hyväksyt Swiftgramin käyttöehdot ja tietosuojakäytännön."; + +"PayWall.Button.OpenPro" = "Käytä Pro-ominaisuuksia"; +"PayWall.Button.Purchasing" = "Ostetaan..."; +"PayWall.Button.Restoring" = "Palautetaan ostot..."; +"PayWall.Button.Validating" = "Ostosten vahvistaminen..."; +"PayWall.Button.PaymentsUnavailable" = "Maksut eivät saatavilla"; +"PayWall.Button.Subscribe" = "Tilaa %@ / kuukausi"; +"PayWall.Button.ContactingAppStore" = "Otetaan yhteyttä App Storeen..."; + +"Paywall.Error.Title" = "Virhe"; +"PayWall.ValidationError" = "Vahvistusvirhe"; +"PayWall.ValidationError.TryAgain" = "Ostovahvistuksessa tapahtui jokin virhe. Ei hätää! Yritä palauttaa ostot hieman myöhemmin."; diff --git a/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..adc9a1b3d3 --- /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 0000000000..eb4562b7c8 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "מגביר את מספר החיבורים המקביליים וגודל חלקי הקבצים. אם הרשת שלך לא יכולה להתמודד עם העומס, נסה אפשרויות שונות שמתאימות לחיבור שלך."; +"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" = "הסתר פאנל תחתון של ערוץ"; + +"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" = "איש קשר משותף"; + +"Settings.swipeForVideoPIP" = "וידאו PIP עם החלקה"; +"Settings.swipeForVideoPIP.Notice" = "אם מופעל, החלקת הווידאו תפתח אותו במצב תמונה בתוך תמונה."; diff --git a/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..6adc148a1d --- /dev/null +++ b/Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "पैरलेल कनेक्शनों की संख्या और फ़ाइल फ़्रैगमेंट का आकार बढ़ाता है। अगर आपका नेटवर्क लोड को संभाल नहीं सकता है, तो अपने कनेक्शन के अनुरूप अलग-अलग विकल्प आजमाएं।"; +"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" = "चैनल बॉटम पैनल छिपाएँ"; + +"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" = "आपसी संपर्क"; + +"Settings.swipeForVideoPIP" = "वीडियो PIP स्वाइप के साथ"; +"Settings.swipeForVideoPIP.Notice" = "यदि सक्षम है, तो वीडियो को स्वाइप करने से यह चित्र-इन-चित्र मोड में खोला जाएगा।"; diff --git a/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..d357bb69b6 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Növeli a párhuzamos kapcsolatok számát és a fájlok darabjainak méretét. Ha a hálózatod nem képes kezelni a terhelést, próbálj ki különböző opciókat, amelyek illeszkednek a kapcsolatodhoz."; +"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"; + +"Settings.CallConfirmation" = "Hívás megerősítése"; +"Settings.CallConfirmation.Notice" = "A Swiftgram megkéri a megerősítését, mielőtt hívást indítana."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Hívást kezdeni?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Videóhívást kezdeni?"; + +"MutualContact.Label" = "közöns kontakt"; + +"Settings.swipeForVideoPIP" = "Videó PIP a húzással"; +"Settings.swipeForVideoPIP.Notice" = "Ha engedélyezve van, a videó húzása képet-képben üzemmódban nyitja meg."; diff --git a/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..44ba7a11a2 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/id.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Meningkatkan jumlah koneksi paralel dan ukuran potongan file. Jika jaringan Anda tidak dapat menangani bebannya, coba berbagai opsi yang sesuai dengan sambungan Anda."; +"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"; + +"Settings.CallConfirmation" = "Konfirmasi Panggilan"; +"Settings.CallConfirmation.Notice" = "Swiftgram akan meminta konfirmasi Anda sebelum melakukan panggilan."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Buat Panggilan?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Buat Panggilan Video?"; + +"MutualContact.Label" = "kontak mutual"; + +"Settings.swipeForVideoPIP" = "Video PIP dengan Geser"; +"Settings.swipeForVideoPIP.Notice" = "Jika diaktifkan, menggeser video akan membukanya dalam mode Gambar-dalam-Gambar."; diff --git a/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..ca32eafb8c --- /dev/null +++ b/Swiftgram/SGStrings/Strings/it.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Aumenta il numero di connessioni parallele e le dimensioni dei frammenti di file. Se la tua rete non riesce a gestire il carico, prova diverse opzioni che si adattano alla tua connessione."; +"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"; + +"Settings.swipeForVideoPIP" = "Video PIP con scorrimento"; +"Settings.swipeForVideoPIP.Notice" = "Se abilitato, scorrendo il video si aprirà in modalità Picture-in-Picture."; diff --git a/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..afe45d6566 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ja.lproj/SGLocalizable.strings @@ -0,0 +1,246 @@ +"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.Notice" = "並行接続の数とファイルチャンクのサイズを増やします。ネットワークが負荷に耐えられない場合は、接続に適した別のオプションを試してください。"; +"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をサポートする全てのアプリでプレミアムメソッドを利用できます。"; +"Common.UpdateOS" = "iOSの更新が必要です"; + +"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" = "ギャラリーのカメラを隠す"; +"Settings.GalleryCameraPreview" = "ギャラリーのカメラプレビュー"; +/* "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.showChannelBottomButton" = "チャンネルボトムパネル"; + +"Settings.secondsInMessages" = "メッセージ内の秒数"; + +"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" = "相互連絡先"; + +"Settings.swipeForVideoPIP" = "ビデオ PIP スワイプ"; +"Settings.swipeForVideoPIP.Notice" = "有効になっている場合、ビデオをスワイプするとピクチャーインピクチャーモードで開きます。"; + +"SessionBackup.Title" = "アカウントのバックアップ"; +"SessionBackup.Sessions.Title" = "セッション"; +"SessionBackup.Actions.Backup" = "キーチェーンにバックアップ"; +"SessionBackup.Actions.Restore" = "キーチェーンから復元"; +"SessionBackup.Actions.DeleteAll" = "キーチェーンのバックアップを削除"; +"SessionBackup.Actions.DeleteOne" = "バックアップから削除"; +"SessionBackup.Actions.RemoveFromApp" = "アプリから削除"; +"SessionBackup.LastBackupAt" = "最終バックアップ: %@"; +"SessionBackup.RestoreOK" = "OK。復元されたセッション: %@"; +"SessionBackup.LoggedIn" = "ログイン中"; +"SessionBackup.LoggedOut" = "ログアウトしました"; +"SessionBackup.DeleteAll.Title" = "すべてのセッションを削除しますか?"; +"SessionBackup.DeleteAll.Text" = "すべてのセッションがキーチェーンから削除されます。\n\nアカウントはSwiftgramからログアウトされません。"; +"SessionBackup.DeleteSingle.Title" = "1つのセッションを削除しますか?"; +"SessionBackup.DeleteSingle.Text" = "%@のセッションがキーチェーンから削除されます。\n\nアカウントはSwiftgramからログアウトされません。"; +"SessionBackup.RemoveFromApp.Title" = "アプリからアカウントを削除しますか?"; +"SessionBackup.RemoveFromApp.Text" = "%@のセッションがSwiftgramから削除されます!セッションはアクティブなままなので、後で復元できます。"; +"SessionBackup.Notice" = "セッションは暗号化され、デバイスのキーチェーンに保存されます。セッションはあなたのデバイスを離れることはありません。\n\n重要: 新しいデバイスまたはOSのリセット後にセッションを復元するには、暗号化されたバックアップを有効にする必要があります。さもなければキーチェーンは移行されません。\n\n注意: セッションはTelegramや他のデバイスからも取り消される可能性があります。"; + +"MessageFilter.Title" = "メッセージフィルター"; +"MessageFilter.SubTitle" = "下記のキーワードを含むメッセージの可視性を減少させ、気を散らさないようにします。\nキーワードは大文字と小文字を区別します。"; +"MessageFilter.Keywords.Title" = "キーワード"; +"MessageFilter.InputPlaceholder" = "キーワードを入力してください"; + +"InputToolbar.Title" = "フォーマットパネル"; + +"Notifications.MentionsAndReplies.Title" = "@メンションと返信"; +"Notifications.MentionsAndReplies.value.default" = "デフォルト"; +"Notifications.MentionsAndReplies.value.silenced" = "ミュート"; +"Notifications.MentionsAndReplies.value.disabled" = "無効"; +"Notifications.PinnedMessages.Title" = "固定メッセージ"; +"Notifications.PinnedMessages.value.default" = "デフォルト"; +"Notifications.PinnedMessages.value.silenced" = "ミュート"; +"Notifications.PinnedMessages.value.disabled" = "無効"; + + +"PayWall.Text" = "プロ機能で強化"; + +"PayWall.SessionBackup.Title" = "アカウントのバックアップ"; +"PayWall.SessionBackup.Notice" = "コードなしでアカウントにログインできます。再インストール後も可能です。デバイス上のキーチェーンで安全に保存されています"; +"PayWall.SessionBackup.Description" = "デバイスを変更したりSwiftgramを削除したりしても、もはや問題にはなりません。Telegramサーバー上でまだアクティブなすべてのセッションを復元します"; + +"PayWall.MessageFilter.Title" = "メッセージフィルター"; +"PayWall.MessageFilter.Notice" = "SPAM、プロモーション、および煩わしいメッセージの可視性を減少させます。"; +"PayWall.MessageFilter.Description" = "見たくないキーワードのリストを作成すると、Swiftgramがそのキーワードを非表示にします"; + +"PayWall.Notifications.Title" = "@メンションと返信を無効にする"; +"PayWall.Notifications.Notice" = "重要でない通知を隠したりミュートしたりします。"; +"PayWall.Notifications.Description" = "気分を落ち着けたいときは、固定メッセージやメンションを非表示にできます"; + +"PayWall.InputToolbar.Title" = "フォーマットパネル"; +"PayWall.InputToolbar.Notice" = "ワンタップでメッセージの書式設定を短縮"; +"PayWall.InputToolbar.Description" = "書式を適用・解除したり、新しい行を挿入したりと、プロのように操作できます"; + +"PayWall.AppIcons.Title" = "ユニークなアプリアイコン"; +"PayWall.AppIcons.Notice" = "ホーム画面でSwiftgramの外観をカスタマイズします。"; + +"PayWall.About.Title" = "Swiftgram Proについて"; +"PayWall.About.Notice" = "Swiftgramの無料版は、Telegramアプリ上で数十の機能と改善を提供します。 毎月のTelegramのアップデートとSwiftgramの同期を革新し、維持することは多くの時間と高価なハードウェアを必要とする膨大な努力です。\n\nSwiftgramはプライバシーを尊重し、広告を気にしないオープンソースのアプリです。 Swiftgram Proに登録すると、排他的な機能にアクセスでき、独立した開発者をサポートできます。"; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "お支払いに問題がありますか?"; +"PayWall.ProSupport.Contact" = "心配ないさ!"; + +"PayWall.RestorePurchases" = "購入を復元する"; +"PayWall.Terms" = "利用規約"; +"PayWall.Privacy" = "プライバシーポリシー"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "Swiftgram Proに購読することで、[Swiftgram利用規約](%1$@)と[プライバシーポリシー](%2$@)に同意したことになります。"; +"PayWall.Notice.Raw" = "Swiftgram Proに購読することで、Swiftgramの利用規約とプライバシーポリシーに同意したことになります。"; + +"PayWall.Button.OpenPro" = "プロ機能を使用する"; +"PayWall.Button.Purchasing" = "購入中…"; +"PayWall.Button.Restoring" = "購入を復元中…"; +"PayWall.Button.Validating" = "購入を検証中…"; +"PayWall.Button.PaymentsUnavailable" = "支払い不可"; +"PayWall.Button.BuyInAppStore" = "App Store版で登録"; +"PayWall.Button.Subscribe" = "%@ / 月で購読"; +"PayWall.Button.ContactingAppStore" = "App Storeに連絡中…"; + +"Paywall.Error.Title" = "エラー"; +"PayWall.ValidationError" = "検証エラー"; +"PayWall.ValidationError.TryAgain" = "購入の検証中に問題が発生しました。心配しないでください!後で購入を復元してみてください。"; +"PayWall.ValidationError.Expired" = "サブスクリプションの有効期限が切れました。Pro機能へのアクセスを取り戻すには、再度サブスクリプションを登録してください。"; diff --git a/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..928cf393a6 --- /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 0000000000..501a5f64b4 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "병렬 연결 수와 파일 조각 크기를 증가시킵니다. 네트워크가 부하를 처리할 수 없는 경우, 연결에 적합한 다양한 옵션을 시도해 보세요."; +"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" = "채널 하단 패널 숨기기"; + +"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" = "상호 연락처"; + +"Settings.swipeForVideoPIP" = "비디오 PIP 스와이프"; +"Settings.swipeForVideoPIP.Notice" = "설정이 활성화되면 비디오를 스와이프하면 화면 속 화면 모드로 열립니다."; diff --git a/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..62ac20a89c --- /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 0000000000..d80e6ca49e --- /dev/null +++ b/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Verhoogt het aantal gelijktijdige verbindingen en de grootte van bestandsgedeelten. Als uw netwerk de belasting niet aankan, probeer dan verschillende opties die geschikt zijn voor uw verbinding."; +"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"; + +"Settings.CallConfirmation" = "Belbevestiging"; +"Settings.CallConfirmation.Notice" = "Swiftgram zal om uw bevestiging vragen voordat er een oproep wordt gedaan."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Een oproep maken?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Een video-oproep maken?"; + +"MutualContact.Label" = "gemeenschappelijke contactpersoon"; + +"Settings.swipeForVideoPIP" = "Video PIP met veeg"; +"Settings.swipeForVideoPIP.Notice" = "Als ingeschakeld, opent het swipen van video het in de modus Beeld-in-Beeld."; diff --git a/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..5fd16d5c62 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/no.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Øker antallet av parallelle forbindelser og størrelsen på filbiter. Hvis nettverket ditt ikke kan håndtere belastningen, prøv forskjellige alternativer som passer til tilkoblingen din."; +"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"; + +"Settings.CallConfirmation" = "Ringebekreftelse"; +"Settings.CallConfirmation.Notice" = "Swiftgram vil spørre om din bekreftelse før det foretas et anrop."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Vil du ringe?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Vil du ta en videosamtale?"; + +"MutualContact.Label" = "gjensidig kontakt"; + +"Settings.swipeForVideoPIP" = "Video PIP med sveip"; +"Settings.swipeForVideoPIP.Notice" = "Hvis aktivert, vil sveipingen av video åpne den i bilde-i-bilde-modus."; diff --git a/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..75194a8c21 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Zwiększa liczbę równoległych połączeń oraz rozmiar fragmentów plików. Jeśli Twoja sieć nie jest w stanie znieść obciążenia, wypróbuj różne opcje, które pasują do Twojego połączenia."; +"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"; + +"Settings.CallConfirmation" = "Potwierdzenie połączenia"; +"Settings.CallConfirmation.Notice" = "Swiftgram poprosi o Twoje potwierdzenie przed wykonaniem połączenia."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Wykonać połączenie?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Wykonać połączenie wideo?"; + +"MutualContact.Label" = "wspólny kontakt"; + +"Settings.swipeForVideoPIP" = "Wideo PIP z przesunięciem"; +"Settings.swipeForVideoPIP.Notice" = "Jeśli włączone, przesunięcie wideo otworzy je w trybie obrazu w obrazie."; diff --git a/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..d63ff19602 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "Aumenta o número de conexões paralelas e o tamanho dos pedaços de arquivo. Se sua rede não conseguir lidar com a carga, tente diferentes opções que se adequem à sua conexão."; +"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."; +"Common.UpdateOS" = "Atualização do iOS necessária"; + +"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"; +"Settings.GalleryCameraPreview" = "Pré-visualização da câmara 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.showChannelBottomButton" = "Painel Inferior do Canal"; + +"Settings.secondsInMessages" = "Segundos em Mensagens"; + +"Settings.CallConfirmation" = "Confirmação de chamada"; +"Settings.CallConfirmation.Notice" = "O Swiftgram pedirá sua confirmação antes de fazer uma chamada."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Fazer uma Chamada?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Fazer uma Chamada de Vídeo?"; + +"MutualContact.Label" = "contato mútuo"; + +"Settings.swipeForVideoPIP" = "Vídeo PIP com Deslizar"; +"Settings.swipeForVideoPIP.Notice" = "Se habilitado, deslizar o vídeo o abrirá em modo Picture-in-Picture."; + +"SessionBackup.Title" = "Backup de Sessão"; +"SessionBackup.Sessions.Title" = "Sessões"; +"SessionBackup.Actions.Backup" = "Backup para o Keychain"; +"SessionBackup.Actions.Restore" = "Restaurar do Keychain"; +"SessionBackup.Actions.DeleteAll" = "Excluir Backup do Keychain"; +"SessionBackup.Actions.DeleteOne" = "Excluir do Backup"; +"SessionBackup.Actions.RemoveFromApp" = "Remover do App"; +"SessionBackup.LastBackupAt" = "Último Backup: %@"; +"SessionBackup.RestoreOK" = "OK. Sessões restauradas: %@"; +"SessionBackup.LoggedIn" = "Conectado"; +"SessionBackup.LoggedOut" = "Desconectado"; +"SessionBackup.DeleteAll.Title" = "Excluir Todas as Sessões?"; +"SessionBackup.DeleteAll.Text" = "Todas as sessões serão removidas do Keychain.\n\nAs contas não serão desconectadas do Swiftgram."; +"SessionBackup.DeleteSingle.Title" = "Excluir 1 (uma) Sessão?"; +"SessionBackup.DeleteSingle.Text" = "%@ sessão será removida do Keychain.\n\nA conta não será desconectada do Swiftgram."; +"SessionBackup.RemoveFromApp.Title" = "Remover conta do App?"; +"SessionBackup.RemoveFromApp.Text" = "%@ sessão SERÁ REMOVIDA do Swiftgram! A sessão permanecerá ativa, para que você possa restaurá-la mais tarde."; +"SessionBackup.Notice" = "As sessões são criptografadas e armazenadas no Acesso às Chaves do dispositivo. As sessões nunca saem do seu dispositivo.\n\nIMPORTANTE: Para restaurar sessões em um novo dispositivo ou após a redefinição do sistema operacional, você DEVE habilitar backups criptografados, caso contrário o Keychain não será transferido.\n\nNOTA: as sessões ainda podem ser revogadas pelo Telegram ou de outro dispositivo."; + +"MessageFilter.Title" = "Filtro de Mensagens"; +"MessageFilter.SubTitle" = "Remova distrações e reduza a visibilidade de mensagens contendo palavras-chave abaixo.\nAs palavras-chave são sensíveis a maiúsculas e minúsculas."; +"MessageFilter.Keywords.Title" = "Palavras-chave"; +"MessageFilter.InputPlaceholder" = "Insira a palavra-chave"; + +"InputToolbar.Title" = "Painel de Formatação"; + +"Notifications.MentionsAndReplies.Title" = "@Menções e Respostas"; +"Notifications.MentionsAndReplies.value.default" = "Padrão"; +"Notifications.MentionsAndReplies.value.silenced" = "Silenciado"; +"Notifications.MentionsAndReplies.value.disabled" = "Desativado"; +"Notifications.PinnedMessages.Title" = "Mensagens Fixadas"; +"Notifications.PinnedMessages.value.default" = "Padrão"; +"Notifications.PinnedMessages.value.silenced" = "Silenciado"; +"Notifications.PinnedMessages.value.disabled" = "Desativado"; + + +"PayWall.Text" = "Supercarregado com recursos Pro"; + +"PayWall.SessionBackup.Title" = "Backup de Sessão"; +"PayWall.SessionBackup.Notice" = "Faça login em contas sem código, mesmo depois de reinstalar. Armazenamento seguro com Keychain no dispositivo."; +"PayWall.SessionBackup.Description" = "Alterar o dispositivo ou excluir o Swiftgram não é mais um problema. Restaure todas as sessões que ainda estão ativas nos servidores do Telegram."; + +"PayWall.MessageFilter.Title" = "Filtro de Mensagens"; +"PayWall.MessageFilter.Notice" = "Reduza a visibilidade de SPAM, promoções e mensagens irritantes."; +"PayWall.MessageFilter.Description" = "Crie uma lista de palavras-chave que você não quer ver frequentemente e o Swiftgram reduzirá as distrações."; + +"PayWall.Notifications.Title" = "Desativar @menções e respostas"; +"PayWall.Notifications.Notice" = "Oculte ou silencie notificações não importantes."; +"PayWall.Notifications.Description" = "Não há mais mensagens fixadas ou @menções quando você precisa de alguma coisa."; + +"PayWall.InputToolbar.Title" = "Painel de Formatação"; +"PayWall.InputToolbar.Notice" = "Economize tempo formatando as mensagens com apenas um único toque."; +"PayWall.InputToolbar.Description" = "Aplique e limpe a formatação ou insira novas linhas como um profissional."; + +"PayWall.AppIcons.Title" = "Ícones de Aplicativos Exclusivos"; +"PayWall.AppIcons.Notice" = "Personalize a aparência do Swiftgram na sua tela inicial."; + +"PayWall.About.Title" = "Sobre o Swiftgram Pro"; +"PayWall.About.Notice" = "A versão gratuita do Swiftgram oferece dezenas de recursos e melhorias em relação ao aplicativo Telegram. Inovar e manter o Swiftgram em sincronia com as atualizações mensais do Telegram é um grande esforço que requer muito tempo e hardware caro.\n\nO Swiftgram é um aplicativo de código aberto que respeita sua privacidade e não incomoda você com anúncios. Ao se inscrever no Swiftgram Pro, você obtém acesso a recursos exclusivos e apoia um desenvolvedor independente."; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "Problemas com pagamento?"; +"PayWall.ProSupport.Contact" = "Não se preocupe!"; + +"PayWall.RestorePurchases" = "Restaurar Compras"; +"PayWall.Terms" = "Termos de Serviço"; +"PayWall.Privacy" = "Política de Privacidade"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "Ao se inscrever no Swiftgram Pro, você concorda com os [Termos de Serviço do Swiftgram](%1$@) e com a [Política de Privacidade](%2$@)."; +"PayWall.Notice.Raw" = "Ao se inscrever no Swiftgram Pro, você concorda com os Termos de Serviço e Política de Privacidade do Swiftgram."; + +"PayWall.Button.OpenPro" = "Usar recursos Pro"; +"PayWall.Button.Purchasing" = "Adquirindo..."; +"PayWall.Button.Restoring" = "Restaurando Compras..."; +"PayWall.Button.Validating" = "Validando Compra..."; +"PayWall.Button.PaymentsUnavailable" = "Pagamentos indisponíveis"; +"PayWall.Button.BuyInAppStore" = "Inscrever-se na versão da App Store"; +"PayWall.Button.Subscribe" = "Assinar por %@ / mês"; +"PayWall.Button.ContactingAppStore" = "Contatando App Store..."; + +"Paywall.Error.Title" = "Erro"; +"PayWall.ValidationError" = "Erro de Validação"; +"PayWall.ValidationError.TryAgain" = "Algo deu errado durante a validação da compra. Sem problemas! Tente Restaurar Compras um pouco mais tarde."; +"PayWall.ValidationError.Expired" = "Sua assinatura expirou. Inscreva-se novamente para recuperar o acesso aos recursos Pro."; diff --git a/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..ccb2ad1e46 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Crește numărul de conexiuni paralele și dimensiunea fragmentelor de fișier. Dacă rețeaua ta nu poate gestiona încărcătura, încearcă diferite opțiuni care se potrivesc conexiunii tale."; +"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"; + +"Settings.CallConfirmation" = "Confirmare apel"; +"Settings.CallConfirmation.Notice" = "Swiftgram va solicita confirmarea dumneavoastră înainte de a efectua un apel."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Ești sigur că vrei să faci un apel?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Ești sigur că vrei să faci un apel video?"; + +"MutualContact.Label" = "contact mutual"; + +"Settings.swipeForVideoPIP" = "Video PIP cu gestul de glisare"; +"Settings.swipeForVideoPIP.Notice" = "Dacă este activat, glisarea video va deschide în modul imagine în imagine."; diff --git a/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..1dad777f9d --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ru.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "Увеличивает количество параллельных соединений и размер частей файлов. Если ваша сеть не может справиться с нагрузкой, попробуйте разные опции, которые подойдут для вашего соединения."; +"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."; +"Common.UpdateOS" = "Требуется обновление iOS"; + +"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" = "Камера в галерее"; +"Settings.GalleryCameraPreview" = "Превью камеры в галерее"; +/* "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.showChannelBottomButton" = "Нижняя панель канала"; + +"Settings.secondsInMessages" = "Секунды в Сообщениях"; + +"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" = "взаимный контакт"; + +"Settings.swipeForVideoPIP" = "Видео PIP свайпом"; +"Settings.swipeForVideoPIP.Notice" = "Если включено, свайп видео откроет его в режиме «Картинка в картинке»."; + +"SessionBackup.Title" = "Бэкап аккаунтов"; +"SessionBackup.Sessions.Title" = "Сессии"; +"SessionBackup.Actions.Backup" = "Бэкап в Keychain"; +"SessionBackup.Actions.Restore" = "Восстановить из Keychain"; +"SessionBackup.Actions.DeleteAll" = "Удалить Бэкап из Keychain"; +"SessionBackup.Actions.DeleteOne" = "Удалить из Бэкапа"; +"SessionBackup.Actions.RemoveFromApp" = "Удалить из приложения"; +"SessionBackup.LastBackupAt" = "Последний бэкап: %@"; +"SessionBackup.RestoreOK" = "ОК. Восстановлено: %@"; +"SessionBackup.LoggedIn" = "Залогинен"; +"SessionBackup.LoggedOut" = "Разлогинен"; +"SessionBackup.DeleteAll.Title" = "Удалить все сессии?"; +"SessionBackup.DeleteAll.Text" = "Все сессии будут удалены из Keychain.\n\nАккаунты не будут разлогинены из Swiftgram."; +"SessionBackup.DeleteSingle.Title" = "Удалить 1 (одну) сессию?"; +"SessionBackup.DeleteSingle.Text" = "%@ сессия будет удалена из Keychain.\n\nАккаунт не будет разлогинен из Swiftgram."; +"SessionBackup.RemoveFromApp.Title" = "Удалить аккаунт из приложения?"; +"SessionBackup.RemoveFromApp.Text" = "%@ сессия БУДЕТ УДАЛЕНА из Swiftgram! Сессия останется активной, чтобы вы могли восстановить ее позже."; +"SessionBackup.Notice" = "Сессии шифруются и хранятся в Keychain устройства. Сессии никогда не покидают ваше устройство.\n\nВАЖНО: Чтобы восстановить сессии на новом устройстве или после сброса системы, ОБЯЗАТЕЛЬНО включите шифрование резервных копий ОС, иначе Keychain будет утерян при восстановлении.\n\nПРИМЕЧАНИЕ: Сессии всё ещё могут быть разлогинены самим Telegram или с другого устройства."; + +"MessageFilter.Title" = "Фильтр сообщений"; +"MessageFilter.SubTitle" = "Убирает отвлекающие факторы и уменьшает видимость сообщений, содержащих ключевые слова ниже.\nКлючевые слова чувствительны к регистру."; +"MessageFilter.Keywords.Title" = "Ключевые слова"; +"MessageFilter.InputPlaceholder" = "Введите слово"; + +"InputToolbar.Title" = "Панель форматирования"; + +"Notifications.MentionsAndReplies.Title" = "@Упоминания и ответы"; +"Notifications.MentionsAndReplies.value.default" = "По умолчанию"; +"Notifications.MentionsAndReplies.value.silenced" = "Без звука"; +"Notifications.MentionsAndReplies.value.disabled" = "Выключено"; +"Notifications.PinnedMessages.Title" = "Сообщение закреплено"; +"Notifications.PinnedMessages.value.default" = "По умолчанию"; +"Notifications.PinnedMessages.value.silenced" = "Без звука"; +"Notifications.PinnedMessages.value.disabled" = "Выключено"; + + +"PayWall.Text" = "Заряжен Pro функциями"; + +"PayWall.SessionBackup.Title" = "Бэкап Аккаунтов"; +"PayWall.SessionBackup.Notice" = "Вход в аккаунты без кода, даже после переустановки. Безопасное хранение в Keychain на устройстве."; +"PayWall.SessionBackup.Description" = "Смена устройства или удаление Swiftgram больше не проблема. Восстановите все ваши Сессии, которые активны на серверах Telegram."; + +"PayWall.MessageFilter.Title" = "Фильтр сообщений"; +"PayWall.MessageFilter.Notice" = "Уменьшает видимость навязчивых сообщений со СПАМом или рекламой."; +"PayWall.MessageFilter.Description" = "Создайте список ключевых слов, которые вы не хотите встречать, и Swiftgram снизит их видимость."; + +"PayWall.Notifications.Title" = "Отключение @тэгов и ответов"; +"PayWall.Notifications.Notice" = "Скрывает или приглушает неважные уведомления."; +"PayWall.Notifications.Description" = "Никаких больше Закрепов или @тэгов, когда нужно побыть в тишине."; + +"PayWall.InputToolbar.Title" = "Панель форматирования"; +"PayWall.InputToolbar.Notice" = "Экономит время, форматируя сообщения всего одним касанием."; +"PayWall.InputToolbar.Description" = "Применяйте и очищайте форматирование, переносите абзацы как Pro."; + +"PayWall.AppIcons.Title" = "Уникальные иконки приложения"; +"PayWall.AppIcons.Notice" = "Настройка внешнего вида Swiftgram на главном экране."; + +"PayWall.About.Title" = "О Swiftgram Pro"; +"PayWall.About.Notice" = "Бесплатная версия Swiftgram предлагает десятки функций и улучшений по сравнению с приложением Telegram. Новые функции и синхронизация Swiftgram с ежемесячными обновлениями Telegram — это огромные усилия, требующие много времени и дорогой техники.\n\nSwiftgram — это приложение с открытым исходным кодом, которое уважает вашу конфиденциальность и не беспокоит вас рекламой. Подписываясь на Swiftgram Pro, вы получаете доступ к эксклюзивным функциям и поддерживаете независимого разработчика."; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "Проблемы с оплатой?"; +"PayWall.ProSupport.Contact" = "Не беда!"; + +"PayWall.RestorePurchases" = "Восстановить покупки"; +"PayWall.Terms" = "Условия использования"; +"PayWall.Privacy" = "Политика конфиденциальности"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "Подписываясь на Swiftgram Pro, вы соглашаетесь с [Условиями использования Swiftgram](%1$@) и [Политикой конфиденциальности](%2$@)."; +"PayWall.Notice.Raw" = "Подписываясь на Swiftgram Pro, вы соглашаетесь с Условиями использования и Политикой конфиденциальности Swiftgram."; + +"PayWall.Button.OpenPro" = "Pro функции"; +"PayWall.Button.Purchasing" = "Покупка..."; +"PayWall.Button.Restoring" = "Восстановление покупок..."; +"PayWall.Button.Validating" = "Проверка покупки..."; +"PayWall.Button.PaymentsUnavailable" = "Платежи недоступны"; +"PayWall.Button.BuyInAppStore" = "Подписаться в App Store версии"; +"PayWall.Button.Subscribe" = "Подписаться за %@ / месяц"; +"PayWall.Button.ContactingAppStore" = "Подключение к App Store..."; + +"Paywall.Error.Title" = "Ошибка"; +"PayWall.ValidationError" = "Ошибка проверки"; +"PayWall.ValidationError.TryAgain" = "Что-то пошло не так во время проверки оплаты. Не волнуйтесь! Попробуйте Восстановить Покупки чуть позже."; +"PayWall.ValidationError.Expired" = "Ваша подписка истекла. Подпишитесь снова, чтобы восстановить доступ к Pro функциям."; diff --git a/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..869c70ba7e --- /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 0000000000..77376339e3 --- /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 0000000000..c71efa9f16 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Увећава број паралелних веза и величину делова фајлова. Уколико ваша мрежа не може да поднесе оптерећење, испробајте различите опције које одговарају вашој вези."; +"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" = "Сакриј донји панел канала"; + +"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" = "заједнички контакт"; + +"Settings.swipeForVideoPIP" = "Видео PIP са свлачење"; +"Settings.swipeForVideoPIP.Notice" = "Ако је омогућено, померање видеа ће га отворити у режиму слике у слици."; diff --git a/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..de9ed08295 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sv.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Ökar antalet parallella anslutningar och storleken på filbitar. Om ditt nätverk inte kan hantera belastningen, prova olika alternativ som passar din anslutning."; +"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"; + +"Settings.CallConfirmation" = "Samtalsbekräftelse"; +"Settings.CallConfirmation.Notice" = "Swiftgram kommer att be om din bekräftelse innan ett samtal görs."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Vill du ringa ett samtal?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Vill du göra ett videosamtal?"; + +"MutualContact.Label" = "ömsesidig kontakt"; + +"Settings.swipeForVideoPIP" = "Video PIP med svep"; +"Settings.swipeForVideoPIP.Notice" = "Om aktiverat, kommer svepning av video att öppna det i bild-i-bild-läge."; diff --git a/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..7f1b643ec7 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/tr.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Paralel bağlantıların sayısını ve dosya parçalarının boyutunu artırır. Ağa yük bindiğinde eağa diğer bağlantı seçeneklerini deneyin."; +"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"; + +"Settings.CallConfirmation" = "Arama Onayı"; +"Settings.CallConfirmation.Notice" = "Swiftgram, arama yapmadan önce onayınızı isteyecek."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Arama Yapmak mı?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Video Araması Yapmak mı?"; + +"MutualContact.Label" = "karşılıklı iletişim"; + +"Settings.swipeForVideoPIP" = "Videoyu kaydırarak PIP"; +"Settings.swipeForVideoPIP.Notice" = "Eğer etkinleştirildi ise videoyu kaydırmak, Piksel içinde Piksel modunda açılacaktır."; diff --git a/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..405fcfb869 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/uk.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "Збільшує кількість паралельних з'єднань та розмір частин файлів. Якщо ваша мережа не може витримати навантаження, спробуйте різні опції, які підходять вашому з'єднанню."; +"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."; +"Common.UpdateOS" = "Необхідне оновлення iOS"; + +"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" = "Камера в галереї"; +"Settings.GalleryCameraPreview" = "Попередній перегляд камери в галереї"; +/* "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.showChannelBottomButton" = "Нижня панель у каналах"; + +"Settings.secondsInMessages" = "Секунди в повідомленнях"; + +"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" = "взаємний контакт"; + +"Settings.swipeForVideoPIP" = "Відео PIP зі змахуванням"; +"Settings.swipeForVideoPIP.Notice" = "Якщо увімкнено, змахування відео відкриє його в режимі «Картинка в картинці»."; + +"SessionBackup.Title" = "Резервне копіювання сесії"; +"SessionBackup.Sessions.Title" = "Сесії"; +"SessionBackup.Actions.Backup" = "Резервне копіювання в Keychain"; +"SessionBackup.Actions.Restore" = "Відновлення з Keychain"; +"SessionBackup.Actions.DeleteAll" = "Видалити резервну копію Keychain"; +"SessionBackup.Actions.DeleteOne" = "Видалити з резервної копії"; +"SessionBackup.Actions.RemoveFromApp" = "Видалити з додатку"; +"SessionBackup.LastBackupAt" = "Останнє резервне копіювання: %@"; +"SessionBackup.RestoreOK" = "ОК. Сесії відновлено: %@"; +"SessionBackup.LoggedIn" = "Увійшли"; +"SessionBackup.LoggedOut" = "Вийшли"; +"SessionBackup.DeleteAll.Title" = "Видалити всі сесії?"; +"SessionBackup.DeleteAll.Text" = "Всі сесії будуть видалені з Keychain.\n\nОблікові записи не будуть вийшли зі Swiftgram."; +"SessionBackup.DeleteSingle.Title" = "Видалити 1 (одну) сесію?"; +"SessionBackup.DeleteSingle.Text" = "%@ сесія буде видалена з Keychain.\n\nОбліковий запис не буде вийшов зі Swiftgram."; +"SessionBackup.RemoveFromApp.Title" = "Видалити обліковий запис з додатку?"; +"SessionBackup.RemoveFromApp.Text" = "%@ сесія БУДЕ ВИДАЛЕНА з Swiftgram! Сесія залишиться активною, щоб ви могли відновити її пізніше."; +"SessionBackup.Notice" = "Сесії зашифровані та зберігаються на пристрої. Сесії ніколи не залишають ваш пристрій.\n\nВАЖЛИВО: Для відновлення сесій на іншому пристрої або після скидання налаштувань, ви повинні ввімкнути шифрування резервних копій, в іншому випадку ключі не будуть перенесені.\n\nТАКОЖ: Сесії можуть бути відкликані Telegram або з іншого пристрою."; + +"MessageFilter.Title" = "Фільтр повідомлень"; +"MessageFilter.SubTitle" = "Приховати відволікання та зменшити видимість повідомлень, що містять нижчевказані ключові слова.\nКлючові слова чутливі до регістру."; +"MessageFilter.Keywords.Title" = "Ключові слова"; +"MessageFilter.InputPlaceholder" = "Введіть ключове слово"; + +"InputToolbar.Title" = "Панель форматування"; + +"Notifications.MentionsAndReplies.Title" = "@Згадай та відповіді"; +"Notifications.MentionsAndReplies.value.default" = "Типовий"; +"Notifications.MentionsAndReplies.value.silenced" = "Приглушено"; +"Notifications.MentionsAndReplies.value.disabled" = "Відключено"; +"Notifications.PinnedMessages.Title" = "Закріплені повідомлення"; +"Notifications.PinnedMessages.value.default" = "Типовий"; +"Notifications.PinnedMessages.value.silenced" = "Приглушено"; +"Notifications.PinnedMessages.value.disabled" = "Відключено"; + + +"PayWall.Text" = "Посилений функціями Pro"; + +"PayWall.SessionBackup.Title" = "Резервне копіювання сесії"; +"PayWall.SessionBackup.Notice" = "Вхід до облікових записів без коду, навіть після перевстановлення. Безпечне сховище з Ключарем пристрою."; +"PayWall.SessionBackup.Description" = "Зміна пристрою або видалення Swiftgram більше не проблема. Відновити всі сеанси, які досі активні на серверах Telegram."; + +"PayWall.MessageFilter.Title" = "Фільтр повідомлень"; +"PayWall.MessageFilter.Notice" = "Зменшити видимість СПАМу, реклам та набридливих повідомлень."; +"PayWall.MessageFilter.Description" = "Створити список ключових слів, які ви не хочете бачити часто, а Swiftgram зменшить відволікання."; + +"PayWall.Notifications.Title" = "Вимкнути @згадки та відповіді"; +"PayWall.Notifications.Notice" = "Сховати або приглушити непотрібні сповіщення."; +"PayWall.Notifications.Description" = "Більше не потрібно використовувати прикріплені повідомлення або @згадки, коли ви потребуєте розуму."; + +"PayWall.InputToolbar.Title" = "Панель форматування"; +"PayWall.InputToolbar.Notice" = "Зберігати час форматування повідомлень одним дотиком."; +"PayWall.InputToolbar.Description" = "Застосувати і очистити форматування або вставити нові лінії, як Pro."; + +"PayWall.AppIcons.Title" = "Унікальні значки додатків"; +"PayWall.AppIcons.Notice" = "Налаштуйте вигляд Swiftgram на вашому домашньому екрані."; + +"PayWall.About.Title" = "Про Swiftgram Pro"; +"PayWall.About.Notice" = "Безкоштовна версія Swiftgram надає десятки функцій та покращень у порівнянні з додатком Telegram. Інновації та підтримка Swiftgram в актуальному стані з місячними оновленнями Telegram потребує величезних зусиль, що вимагають багато часу та дорогого обладнання.\n\nSwiftgram — це додаток з відкритим кодом, який поважає вашу конфіденційність і не турбує вас рекламою. Підписуючись на Swiftgram Pro, ви отримуєте доступ до ексклюзивних функцій і підтримуєте незалежного розробника.\n\n- @Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "Проблеми з оплатою?"; +"PayWall.ProSupport.Contact" = "Не хвилюйтеся!"; + +"PayWall.RestorePurchases" = "Відновити покупки"; +"PayWall.Terms" = "Умови обслуговування"; +"PayWall.Privacy" = "Політика конфіденційності"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "Підписуючись на Swiftgram Pro, ви погоджуєтеся з [Умовами обслуговування Swiftgram](%1$@) та [Політикою конфіденційності](%2$@)."; +"PayWall.Notice.Raw" = "Підписуючись на Swiftgram Pro, ви погоджуєтеся з Умовами обслуговування Swiftgram та Політикою конфіденційності."; + +"PayWall.Button.OpenPro" = "Використовувати функції Pro"; +"PayWall.Button.Purchasing" = "Придбання..."; +"PayWall.Button.Restoring" = "Відновлення покупок..."; +"PayWall.Button.Validating" = "Перевірка покупки..."; +"PayWall.Button.PaymentsUnavailable" = "Платежі недоступні"; +"PayWall.Button.BuyInAppStore" = "Підписатися на версію в App Store"; +"PayWall.Button.Subscribe" = "Підписатися за %@ / місяць"; +"PayWall.Button.ContactingAppStore" = "Зв'язок з App Store..."; + +"Paywall.Error.Title" = "Помилка"; +"PayWall.ValidationError" = "Помилка валідації"; +"PayWall.ValidationError.TryAgain" = "Щось пішло не так під час перевірки покупки. Не хвилюйтеся! Спробуйте відновити покупки трохи пізніше."; +"PayWall.ValidationError.Expired" = "Ваша підписка застаріла. Підпишіться, щоб відновити доступ до Pro-можливостей."; diff --git a/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..cfab47bc31 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/uz.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Parallel ulanishlar sonini va fayl bo'laklari o'lchamini oshiradi. Agar sizning tarmog'ingiz yuklamani boshqarolmasa, ulanishingizga mos keladigan boshqa variantlarni sinab ko'ring."; +"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"; + +"Settings.CallConfirmation" = "Qo'ng'iroq tasdiqlanishi"; +"Settings.CallConfirmation.Notice" = "Swiftgram sizdan qo'ng'iroq qilishdan oldin tasdiqlashni so'raydi."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Qo'ng'iroq qilish?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Video qo'ng'iroq qilish?"; + +"MutualContact.Label" = "o'zaro aloqa"; + +"Settings.swipeForVideoPIP" = "Video PIP bilan Surish"; +"Settings.swipeForVideoPIP.Notice" = "Agar yoqilgan bo'lsa, videoni surish uni Tasvir ichida Tasvir rejimida ochadi."; diff --git a/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings new file mode 100644 index 0000000000..8878463be8 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/vi.lproj/SGLocalizable.strings @@ -0,0 +1,152 @@ +"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.Notice" = "Tăng số lượng kết nối song song và kích thước các khối tệp. Nếu mạng của bạn không thể xử lý tải, hãy thử các tùy chọn khác phù hợp với kết nối của bạn."; +"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"; + +"Settings.CallConfirmation" = "Xác nhận cuộc gọi"; +"Settings.CallConfirmation.Notice" = "Swiftgram sẽ yêu cầu bạn xác nhận trước khi thực hiện cuộc gọi."; + +/* Confirmation before making a Call */ +"CallConfirmation.Audio.Title" = "Thực hiện cuộc gọi?"; + +/* Confirmation before making a Video Call */ +"CallConfirmation.Video.Title" = "Thực hiện cuộc gọi video?"; + +"MutualContact.Label" = "liên hệ chung"; + +"Settings.swipeForVideoPIP" = "Video PIP với Vuốt"; +"Settings.swipeForVideoPIP.Notice" = "Nếu được kích hoạt, việc vuốt video sẽ mở nó ở chế độ Hình trong hì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 0000000000..0dfe9dba25 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/zh-hans.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "增加并行连接的数量和文件块的大小。如果您的网络无法承受负载,请尝试不同适合您连接的选项。"; +"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 中生效。"; +"Common.UpdateOS" = "需要 iOS 更新"; + +"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" = "图库中的相机"; +"Settings.GalleryCameraPreview" = "图库中的相机预览"; +/* "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.showChannelBottomButton" = "频道底部面板"; + +"Settings.secondsInMessages" = "消息中的秒数"; + +"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" = "双向联系人"; + +"Settings.swipeForVideoPIP" = "上滑打开画中画"; +"Settings.swipeForVideoPIP.Notice" = "如果启用,滑动视频将以画中画模式打开。"; + +"SessionBackup.Title" = "会话备份"; +"SessionBackup.Sessions.Title" = "会话"; +"SessionBackup.Actions.Backup" = "备份到钥匙串"; +"SessionBackup.Actions.Restore" = "从钥匙串恢复"; +"SessionBackup.Actions.DeleteAll" = "删除钥匙串备份"; +"SessionBackup.Actions.DeleteOne" = "从备份中删除"; +"SessionBackup.Actions.RemoveFromApp" = "从应用程序中移除"; +"SessionBackup.LastBackupAt" = "最后备份:%@"; +"SessionBackup.RestoreOK" = "确定。已恢复会话:%@"; +"SessionBackup.LoggedIn" = "已登录"; +"SessionBackup.LoggedOut" = "已登出"; +"SessionBackup.DeleteAll.Title" = "删除所有会话?"; +"SessionBackup.DeleteAll.Text" = "所有会话将从钥匙串中删除。\n\n帐号将不会从 Swiftgram 登出。"; +"SessionBackup.DeleteSingle.Title" = "删除 1 个会话?"; +"SessionBackup.DeleteSingle.Text" = "%@ 会话将从钥匙串中删除。\n\n帐号将不会从 Swiftgram 登出。"; +"SessionBackup.RemoveFromApp.Title" = "从应用程序中移除帐户?"; +"SessionBackup.RemoveFromApp.Text" = "%@ 会话将从 Swiftgram 中移除!会话将保持活动状态,以便稍后恢复。"; +"SessionBackup.Notice" = "会话已加密并存储在设备的钥匙串中。会话永远不会离开您的设备。\n\n重要提示:要在新设备或操作系统重置后恢复会话,您必须启用加密备份,否则钥匙串将无法转移。\n\n注意:会话仍可能被Telegram或另一台设备撤销。"; + +"MessageFilter.Title" = "消息过滤器"; +"MessageFilter.SubTitle" = "移除干扰,减少包含以下关键字的消息的可见性。\n关键字区分大小写。"; +"MessageFilter.Keywords.Title" = "关键字"; +"MessageFilter.InputPlaceholder" = "输入关键字"; + +"InputToolbar.Title" = "格式面板"; + +"Notifications.MentionsAndReplies.Title" = "@提及和回复"; +"Notifications.MentionsAndReplies.value.default" = "默认"; +"Notifications.MentionsAndReplies.value.silenced" = "已静音"; +"Notifications.MentionsAndReplies.value.disabled" = "停用"; +"Notifications.PinnedMessages.Title" = "置顶消息"; +"Notifications.PinnedMessages.value.default" = "默认"; +"Notifications.PinnedMessages.value.silenced" = "已静音"; +"Notifications.PinnedMessages.value.disabled" = "停用"; + + +"PayWall.Text" = "增强了专业功能"; + +"PayWall.SessionBackup.Title" = "账号备份"; +"PayWall.SessionBackup.Notice" = "即使在重新安装后也可以登录到没有代码的帐户。使用设备上的密钥链来安全存储。"; +"PayWall.SessionBackup.Description" = "更改设备或删除 Swiftgram 已不再是一个问题。还原在Telegram 服务器上仍然活跃的所有会话。"; + +"PayWall.MessageFilter.Title" = "消息过滤器"; +"PayWall.MessageFilter.Notice" = "减少 SPAM、促销和令人烦恼的消息的可见性。"; +"PayWall.MessageFilter.Description" = "创建一个您不想经常看到的关键字列表,而Swiftgram 会减少干扰。"; + +"PayWall.Notifications.Title" = "禁用 @提及和回复"; +"PayWall.Notifications.Notice" = "隐藏或静音不重要的通知。"; +"PayWall.Notifications.Description" = "当你需要一点心情时,不再有固定的消息或 @reference."; + +"PayWall.InputToolbar.Title" = "格式面板"; +"PayWall.InputToolbar.Notice" = "只需单击即可节省时间格式化消息。"; +"PayWall.InputToolbar.Description" = "应用并清除格式化或插入像专业版这样的新行。"; + +"PayWall.AppIcons.Title" = "独特的应用图标"; +"PayWall.AppIcons.Notice" = "自定义 Swiftgram 在主屏幕上的外观。"; + +"PayWall.About.Title" = "关于 Swiftgram Pro"; +"PayWall.About.Notice" = "Swiftgram 的免费版本提供超过 Telegram 应用的多个功能和改进。创新并保持 Swiftgram 与每月的 Telegram 更新同步是一项庞大的工作,需要耗费大量的时间和昂贵的硬件。\n\nSwiftgram 是一个开源应用,尊重您的隐私,并且不打扰您广告。订阅 Swiftgram Pro,您将获得独享特性并支持独立开发者。\n\n- @Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "支付问题?"; +"PayWall.ProSupport.Contact" = "不用担心!"; + +"PayWall.RestorePurchases" = "恢复购买"; +"PayWall.Terms" = "服务条款"; +"PayWall.Privacy" = "隐私政策"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "通过订阅 Swiftgram Pro,您同意 [Swiftgram 服务条款](%1$@) 和 [隐私政策](%2$@)。"; +"PayWall.Notice.Raw" = "通过订阅 Swiftgram Pro,您同意 Swiftgram 服务条款和隐私政策。"; + +"PayWall.Button.OpenPro" = "使用专业功能"; +"PayWall.Button.Purchasing" = "正在购买……"; +"PayWall.Button.Restoring" = "正在恢复购买……"; +"PayWall.Button.Validating" = "正在验证购买……"; +"PayWall.Button.PaymentsUnavailable" = "付款不可用"; +"PayWall.Button.BuyInAppStore" = "订阅 App Store 版本"; +"PayWall.Button.Subscribe" = "订阅 %@ / 月"; +"PayWall.Button.ContactingAppStore" = "正在联系 App Store……"; + +"Paywall.Error.Title" = "错误"; +"PayWall.ValidationError" = "验证错误"; +"PayWall.ValidationError.TryAgain" = "购买验证过程中出现问题。不用担心!稍后再试恢复购买。"; +"PayWall.ValidationError.Expired" = "您的订阅已过期。再次订阅以重新获得专业版功能。"; 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 0000000000..fd7effeb8c --- /dev/null +++ b/Swiftgram/SGStrings/Strings/zh-hant.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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.Notice" = "增加並行連接的數量和文件區塊的大小。如果您的網路無法承受負載,請嘗試不同適合您連接的選項。"; +"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 中可用。"; +"Common.UpdateOS" = "需要 iOS 更新"; + +"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" = "相簿圖庫"; +"Settings.GalleryCameraPreview" = "照片預覽"; +/* "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.showChannelBottomButton" = "頻道底部面板"; + +"Settings.secondsInMessages" = "消息中的秒數"; + +"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" = "雙向聯絡人"; + +"Settings.swipeForVideoPIP" = "影片 PIP 及滑動"; +"Settings.swipeForVideoPIP.Notice" = "如果啟用,滑動視頻將以畫中畫模式打開。"; + +"SessionBackup.Title" = "帳號備份"; +"SessionBackup.Sessions.Title" = "會話"; +"SessionBackup.Actions.Backup" = "備份到鑰匙串"; +"SessionBackup.Actions.Restore" = "從鑰匙串還原"; +"SessionBackup.Actions.DeleteAll" = "刪除鑰匙串備份"; +"SessionBackup.Actions.DeleteOne" = "從備份刪除"; +"SessionBackup.Actions.RemoveFromApp" = "從應用中移除"; +"SessionBackup.LastBackupAt" = "最後備份時間: %@"; +"SessionBackup.RestoreOK" = "確定。還原的會話: %@"; +"SessionBackup.LoggedIn" = "已登錄"; +"SessionBackup.LoggedOut" = "已登出"; +"SessionBackup.DeleteAll.Title" = "刪除所有會話?"; +"SessionBackup.DeleteAll.Text" = "所有會話將從鑰匙串中移除。\n\n帳戶將不會從 Swiftgram 登出。"; +"SessionBackup.DeleteSingle.Title" = "刪除 1 (一) 會話?"; +"SessionBackup.DeleteSingle.Text" = "%@ 會話將從鑰匙串中移除。\n\n帳戶將不會從 Swiftgram 登出。"; +"SessionBackup.RemoveFromApp.Title" = "從應用中移除帳戶?"; +"SessionBackup.RemoveFromApp.Text" = "%@ 會話將從 Swiftgram 中移除!會話將保持活躍,以便您稍後恢復。"; +"SessionBackup.Notice" = "會話會被加密並儲存在設備的鑰匙圈中。會話從不離開您的設備。\n\n重要提示:要在新設備上或在操作系統重置後恢復會話,您必須啟用加密備份,否則鑰匙圈將無法轉移。\n\n注意:會話仍然可能被 Telegram 或其他設備撤銷。"; + +"MessageFilter.Title" = "訊息過濾器"; +"MessageFilter.SubTitle" = "移除干擾並減少包含以下關鍵字的訊息的可見性。\n關鍵字區分大小寫。"; +"MessageFilter.Keywords.Title" = "關鍵字"; +"MessageFilter.InputPlaceholder" = "輸入關鍵字"; + +"InputToolbar.Title" = "格式化面板"; + +"Notifications.MentionsAndReplies.Title" = "@提及和回覆"; +"Notifications.MentionsAndReplies.value.default" = "預設"; +"Notifications.MentionsAndReplies.value.silenced" = "靜音"; +"Notifications.MentionsAndReplies.value.disabled" = "已停用"; +"Notifications.PinnedMessages.Title" = "置頂訊息"; +"Notifications.PinnedMessages.value.default" = "預設"; +"Notifications.PinnedMessages.value.silenced" = "靜音"; +"Notifications.PinnedMessages.value.disabled" = "已停用"; + + +"PayWall.Text" = "以 Pro 功能強化"; + +"PayWall.SessionBackup.Title" = "帳號備份"; +"PayWall.SessionBackup.Notice" = "即使在重新安裝後也可以登錄到沒有代碼的帳戶。使用設備上的密鑰鏈來安全存儲。"; +"PayWall.SessionBackup.Description" = "更改設備或刪除 Swiftgram 不再是問題。恢復 Telegram 伺服器上仍然活躍的所有會話。"; + +"PayWall.MessageFilter.Title" = "訊息過濾器"; +"PayWall.MessageFilter.Notice" = "減少 SPAM、促銷和煩人的訊息的可見性。"; +"PayWall.MessageFilter.Description" = "建立一個不想經常看到的關鍵字列表,Swiftgram 將減少干擾。"; + +"PayWall.Notifications.Title" = "禁用 @提及和回覆"; +"PayWall.Notifications.Notice" = "隱藏或靜音不重要的通知。"; +"PayWall.Notifications.Description" = "當你需要一些心情時,不再有固定的訊息或 @提及。"; + +"PayWall.InputToolbar.Title" = "格式化面板"; +"PayWall.InputToolbar.Notice" = "只需輕點即可節省時間格式化訊息。"; +"PayWall.InputToolbar.Description" = "像專業人士一樣應用或清除格式化,或插入新行。"; + +"PayWall.AppIcons.Title" = "獨特的應用圖標"; +"PayWall.AppIcons.Notice" = "自訂 Swiftgram 在您的主屏幕上的外觀。"; + +"PayWall.About.Title" = "關於 Swiftgram Pro"; +"PayWall.About.Notice" = "Swiftgram 免費版本提供比 Telegram 應用更多的功能和改進。創新和保持 Swiftgram 與 Telegram 更新同步是一項巨大的努力,需要大量的時間和昂貴的硬體。\n\nSwiftgram 是一個尊重您隱私且不會打擾您廣告的開源應用。訂閱 Swiftgram Pro,您可以訪問獨家功能並支持獨立開發者。"; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "支付問題?"; +"PayWall.ProSupport.Contact" = "不用擔心!"; + +"PayWall.RestorePurchases" = "恢復購買"; +"PayWall.Terms" = "服務條款"; +"PayWall.Privacy" = "隱私政策"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "通過訂閱 Swiftgram Pro,您同意[Swiftgram 服務條款](%1$@)和[隱私政策](%2$@)。"; +"PayWall.Notice.Raw" = "通過訂閱 Swiftgram Pro,您同意 Swiftgram 服務條款和隱私政策。"; + +"PayWall.Button.OpenPro" = "使用 Pro 功能"; +"PayWall.Button.Purchasing" = "購買中……"; +"PayWall.Button.Restoring" = "恢復購買中……"; +"PayWall.Button.Validating" = "驗證購買中……"; +"PayWall.Button.PaymentsUnavailable" = "付款不可用"; +"PayWall.Button.BuyInAppStore" = "訂閱 App Store 版本"; +"PayWall.Button.Subscribe" = "訂閱 %@ / 月"; +"PayWall.Button.ContactingAppStore" = "正在聯繫 App Store……"; + +"Paywall.Error.Title" = "錯誤"; +"PayWall.ValidationError" = "驗證錯誤"; +"PayWall.ValidationError.TryAgain" = "在購買驗證過程中出錯。別擔心!稍後再試恢復購買。"; +"PayWall.ValidationError.Expired" = "您的訂閱已過期。請重新訂閱以恢復訪問 Pro 功能。"; diff --git a/Swiftgram/SGSwiftSignalKit/BUILD b/Swiftgram/SGSwiftSignalKit/BUILD new file mode 100644 index 0000000000..ed4f4a6081 --- /dev/null +++ b/Swiftgram/SGSwiftSignalKit/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGSwiftSignalKit", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift b/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift new file mode 100644 index 0000000000..51b104adef --- /dev/null +++ b/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift @@ -0,0 +1,134 @@ +import Foundation + +public func transformValue(_ f: @escaping(T) -> R) -> (Signal) -> Signal { + return map(f) +} + +public func transformValueToSignal(_ f: @escaping(T) -> Signal) -> (Signal) -> Signal { + return mapToSignal(f) +} + +public func convertSignalWithNoErrorToSignalWithError(_ f: @escaping(T) -> Signal) -> (Signal) -> Signal { + return mapToSignalPromotingError(f) +} + +public func ignoreSignalErrors(onError: ((E) -> Void)? = nil) -> (Signal) -> Signal { + return { signal in + return signal |> `catch` { error in + // Log the error using the provided callback, if any + onError?(error) + + // Returning a signal that completes without errors + return Signal { subscriber in + subscriber.putCompletion() + return EmptyDisposable + } + } + } +} + +// Wrapper for non-Error types +public struct SignalError: Error { + public let error: E + + public init(_ error: E) { + self.error = error + } +} + +public struct SignalCompleted: Error {} + +// Extension for Signals +// NoError can be marked a +// try? await signal.awaitable() +extension Signal { + @available(iOS 13.0, *) + public func awaitable(file: String = #file, line: Int = #line) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + var disposable: Disposable? + let hasResumed = Atomic(value: false) + disposable = self.start( + next: { value in + if !hasResumed.with({ $0 }) { + let _ = hasResumed.swap(true) + continuation.resume(returning: value) + } else { + #if DEBUG + // Consider using awaitableStream() or |> take(1) + assertionFailure("awaitable Signal emitted more than one value. \(file):\(line)") + #endif + } + disposable?.dispose() + }, + error: { error in + if !hasResumed.with({ $0 }) { + let _ = hasResumed.swap(true) + if let error = error as? Error { + continuation.resume(throwing: error) + } else { + continuation.resume(throwing: SignalError(error)) + } + } else { + #if DEBUG + // I don't even know what we should consider here. awaitableStream? + assertionFailure("awaitable Signal emitted an error after a value. \(file):\(line)") + #endif + } + disposable?.dispose() + }, + completed: { + if !hasResumed.with({ $0 }) { + let _ = hasResumed.swap(true) + continuation.resume(throwing: SignalCompleted()) + } + disposable?.dispose() + } + ) + } + } +} + +// Extension for general Signal types - AsyncStream support +extension Signal { + @available(iOS 13.0, *) + public func awaitableStream() -> AsyncStream { + return AsyncStream { continuation in + let disposable = self.start( + next: { value in + continuation.yield(value) + }, + error: { _ in + continuation.finish() + }, + completed: { + continuation.finish() + } + ) + + continuation.onTermination = { @Sendable _ in + disposable.dispose() + } + } + } +} + +// Extension for NoError Signal types - AsyncStream support +extension Signal where E == NoError { + @available(iOS 13.0, *) + public func awaitableStream() -> AsyncStream { + return AsyncStream { continuation in + let disposable = self.start( + next: { value in + continuation.yield(value) + }, + completed: { + continuation.finish() + } + ) + + continuation.onTermination = { @Sendable _ in + disposable.dispose() + } + } + } +} diff --git a/Swiftgram/SGSwiftUI/BUILD b/Swiftgram/SGSwiftUI/BUILD new file mode 100644 index 0000000000..9437ba2d57 --- /dev/null +++ b/Swiftgram/SGSwiftUI/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGSwiftUI", + module_name = "SGSwiftUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + "//submodules/LegacyUI:LegacyUI", + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift new file mode 100644 index 0000000000..066174d814 --- /dev/null +++ b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift @@ -0,0 +1,513 @@ +import Display +import Foundation +import LegacyUI +import SwiftUI +import TelegramPresentationData + + +@available(iOS 13.0, *) +public class ObservedValue: ObservableObject { + @Published public var value: T + + public init(_ value: T) { + self.value = value + } +} + +@available(iOS 13.0, *) +public struct NavigationBarHeightKey: EnvironmentKey { + public static let defaultValue: CGFloat = 0 +} + +@available(iOS 13.0, *) +public struct ContainerViewLayoutKey: EnvironmentKey { + public static let defaultValue: ContainerViewLayout? = nil +} + +@available(iOS 13.0, *) +public struct LangKey: EnvironmentKey { + public static let defaultValue: String = "en" +} + +// Perhaps, affects Performance a lot +//@available(iOS 13.0, *) +//public struct ContainerViewLayoutUpdateCountKey: EnvironmentKey { +// public static let defaultValue: ObservedValue = ObservedValue(0) +//} + +@available(iOS 13.0, *) +public extension EnvironmentValues { + var navigationBarHeight: CGFloat { + get { self[NavigationBarHeightKey.self] } + set { self[NavigationBarHeightKey.self] = newValue } + } + + var containerViewLayout: ContainerViewLayout? { + get { self[ContainerViewLayoutKey.self] } + set { self[ContainerViewLayoutKey.self] = newValue } + } + + var lang: String { + get { self[LangKey.self] } + set { self[LangKey.self] = newValue } + } + +// var containerViewLayoutUpdateCount: ObservedValue { +// get { self[ContainerViewLayoutUpdateCountKey.self] } +// set { self[ContainerViewLayoutUpdateCountKey.self] = newValue } +// } +} + + +@available(iOS 13.0, *) +public struct SGSwiftUIView: View { + public let content: Content + public let manageSafeArea: Bool + + @ObservedObject var navigationBarHeight: ObservedValue + @ObservedObject var containerViewLayout: ObservedValue +// @ObservedObject var containerViewLayoutUpdateCount: ObservedValue + + private var lang: String + + public init( + legacyController: LegacySwiftUIController, + manageSafeArea: Bool = false, + @ViewBuilder content: () -> Content + ) { + #if DEBUG + if manageSafeArea { + print("WARNING SGSwiftUIView: manageSafeArea is deprecated, use @Environment(\\.navigationBarHeight) and @Environment(\\.containerViewLayout)") + } + #endif + self.navigationBarHeight = legacyController.navigationBarHeightModel + self.containerViewLayout = legacyController.containerViewLayoutModel + self.lang = legacyController.lang +// self.containerViewLayoutUpdateCount = legacyController.containerViewLayoutUpdateCountModel + self.manageSafeArea = manageSafeArea + self.content = content() + } + + public var body: some View { + content + .if(manageSafeArea) { $0.modifier(CustomSafeArea()) } + .environment(\.navigationBarHeight, navigationBarHeight.value) + .environment(\.containerViewLayout, containerViewLayout.value) + .environment(\.lang, lang) +// .environment(\.containerViewLayoutUpdateCount, containerViewLayoutUpdateCount) +// .onReceive(containerViewLayoutUpdateCount.$value) { _ in +// // Make sure View is updated when containerViewLayoutUpdateCount changes, +// // in case it does not depend on containerViewLayout +// } + } + +} + +@available(iOS 13.0, *) +public struct CustomSafeArea: ViewModifier { + @Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat + @Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout? + + public func body(content: Content) -> some View { + content + .edgesIgnoringSafeArea(.all) +// .padding(.top, /*totalTopSafeArea > navigationBarHeight.value ? totalTopSafeArea :*/ navigationBarHeight.value) + .padding(.top, topInset) + .padding(.bottom, bottomInset) + .padding(.leading, leftInset) + .padding(.trailing, rightInset) + } + + private var topInset: CGFloat { + max( + (containerViewLayout?.safeInsets.top ?? 0) + (containerViewLayout?.intrinsicInsets.top ?? 0), + navigationBarHeight + ) + } + + private var bottomInset: CGFloat { + (containerViewLayout?.safeInsets.bottom ?? 0) +// DEPRECATED, do not change +// + (containerViewLayout.value?.intrinsicInsets.bottom ?? 0) + } + + private var leftInset: CGFloat { + containerViewLayout?.safeInsets.left ?? 0 + } + + private var rightInset: CGFloat { + containerViewLayout?.safeInsets.right ?? 0 + } +} + +@available(iOS 13.0, *) +public extension View { + func sgTopSafeAreaInset(_ containerViewLayout: ContainerViewLayout?, _ navigationBarHeight: CGFloat) -> CGFloat { + return max( + (containerViewLayout?.safeInsets.top ?? 0) + (containerViewLayout?.intrinsicInsets.top ?? 0), + navigationBarHeight + ) + } + + func sgBottomSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat { + return (containerViewLayout?.safeInsets.bottom ?? 0) + (containerViewLayout?.intrinsicInsets.bottom ?? 0) + } + + func sgLeftSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat { + return containerViewLayout?.safeInsets.left ?? 0 + } + + func sgRightSafeAreaInset(_ containerViewLayout: ContainerViewLayout?) -> CGFloat { + return containerViewLayout?.safeInsets.right ?? 0 + } + +} + + +@available(iOS 13.0, *) +public final class LegacySwiftUIController: LegacyController { + public var navigationBarHeightModel: ObservedValue + public var containerViewLayoutModel: ObservedValue + public var inputHeightModel: ObservedValue + public let lang: String +// public var containerViewLayoutUpdateCountModel: ObservedValue + + override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { + navigationBarHeightModel = ObservedValue(0.0) + containerViewLayoutModel = ObservedValue(initialLayout) + inputHeightModel = ObservedValue(nil) + lang = strings?.baseLanguageCode ?? "en" +// containerViewLayoutUpdateCountModel = ObservedValue(0) + super.init(presentation: presentation, theme: theme, strings: strings, initialLayout: initialLayout) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) +// containerViewLayoutUpdateCountModel.value += 1 + + var newNavigationBarHeight = navigationLayout(layout: layout).navigationFrame.maxY + if !self.displayNavigationBar || self.navigationPresentation == .modal { + newNavigationBarHeight = 0.0 + } + if navigationBarHeightModel.value != newNavigationBarHeight { + navigationBarHeightModel.value = newNavigationBarHeight + } + if containerViewLayoutModel.value != layout { + containerViewLayoutModel.value = layout + } + if inputHeightModel.value != layout.inputHeight { + inputHeightModel.value = layout.inputHeight + } + } + + override public func bind(controller: UIViewController) { + super.bind(controller: controller) + addChild(legacyController) + legacyController.didMove(toParent: legacyController) + } + + @available(*, unavailable) + public required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@available(iOS 13.0, *) +extension UIHostingController { + public convenience init(rootView: Content, ignoreSafeArea: Bool) { + self.init(rootView: rootView) + + if ignoreSafeArea { + disableSafeArea() + } + } + + func disableSafeArea() { + guard let viewClass = object_getClass(view) else { + return + } + + func encodeText(string: String, key: Int16) -> String { + let nsString = string as NSString + let result = NSMutableString() + for i in 0 ..< nsString.length { + var c: unichar = nsString.character(at: i) + c = unichar(Int16(c) + key) + result.append(NSString(characters: &c, length: 1) as String) + } + return result as String + } + + let viewSubclassName = String(cString: class_getName(viewClass)).appending(encodeText(string: "`JhopsfTbgfBsfb", key: -1)) + + if let viewSubclass = NSClassFromString(viewSubclassName) { + object_setClass(view, viewSubclass) + } else { + guard + let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, + let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) + else { + return + } + + if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { + let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in + .zero + } + + class_addMethod( + viewSubclass, + #selector(getter: UIView.safeAreaInsets), + imp_implementationWithBlock(safeAreaInsets), + method_getTypeEncoding(method) + ) + } + + objc_registerClassPair(viewSubclass) + object_setClass(view, viewSubclass) + } + } +} + + +@available(iOS 13.0, *) +public struct TGNavigationBackButtonModifier: ViewModifier { + weak var wrapperController: LegacyController? + + public func body(content: Content) -> some View { + content + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: + NavigationBarBackButton(action: { + wrapperController?.dismiss() + }) + .padding(.leading, -8) + ) + } +} + +@available(iOS 13.0, *) +public extension View { + func tgNavigationBackButton(wrapperController: LegacyController?) -> some View { + modifier(TGNavigationBackButtonModifier(wrapperController: wrapperController)) + } +} + + +@available(iOS 13.0, *) +public struct NavigationBarBackButton: View { + let text: String + let color: Color + let action: () -> Void + + public init(text: String = "Back", color: Color = .accentColor, action: @escaping () -> Void) { + self.text = text + self.color = color + self.action = action + } + + public var body: some View { + Button(action: action) { + HStack(spacing: 6) { + if let customBackArrow = NavigationBar.backArrowImage(color: color.uiColor()) { + Image(uiImage: customBackArrow) + } else { + Image(systemName: "chevron.left") + .font(Font.body.weight(.bold)) + .foregroundColor(color) + } + Text(text) + .foregroundColor(color) + } + .contentShape(Rectangle()) + } + } +} + +@available(iOS 13.0, *) +public extension View { + func apply(@ViewBuilder _ block: (Self) -> V) -> V { block(self) } + + @ViewBuilder + func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + @ViewBuilder + func `if`(_ condition: @escaping () -> Bool, transform: (Self) -> Content) -> some View { + if condition() { + transform(self) + } else { + self + } + } +} + +@available(iOS 13.0, *) +public extension Color { + + func uiColor() -> UIColor { + + if #available(iOS 14.0, *) { + return UIColor(self) + } + + let components = self.components() + return UIColor(red: components.r, green: components.g, blue: components.b, alpha: components.a) + } + + private func components() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + + let scanner = Scanner(string: self.description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)) + var hexNumber: UInt64 = 0 + var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0 + + let result = scanner.scanHexInt64(&hexNumber) + if result { + r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 + b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 + a = CGFloat(hexNumber & 0x000000ff) / 255 + } + return (r, g, b, a) + } + + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 6: // RGB (No alpha) + (a, r, g, b) = (255, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff) + case 8: // ARGB + (a, r, g, b) = ((int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(.sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255) + } +} + + +public enum BackgroundMaterial { + case ultraThinMaterial + case thinMaterial + case regularMaterial + case thickMaterial + case ultraThickMaterial + + @available(iOS 15.0, *) + var material: Material { + switch self { + case .ultraThinMaterial: return .ultraThinMaterial + case .thinMaterial: return .thinMaterial + case .regularMaterial: return .regularMaterial + case .thickMaterial: return .thickMaterial + case .ultraThickMaterial: return .ultraThickMaterial + } + } +} + +public enum BounceBehavior { + case automatic + case always + case basedOnSize + + @available(iOS 16.4, *) + var behavior: ScrollBounceBehavior { + switch self { + case .automatic: return .automatic + case .always: return .always + case .basedOnSize: return .basedOnSize + } + } +} + + +@available(iOS 13.0, *) +public extension View { + func fontWeightIfAvailable(_ weight: SwiftUI.Font.Weight) -> some View { + if #available(iOS 16.0, *) { + return self.fontWeight(weight) + } else { + return self + } + } + + func backgroundIfAvailable(material: BackgroundMaterial) -> some View { + if #available(iOS 15.0, *) { + return self.background(material.material) + } else { + return self.background( + Color(.systemBackground) + .opacity(0.75) + .blur(radius: 3) + .overlay(Color.white.opacity(0.1)) + ) + } + } +} + +@available(iOS 13.0, *) +public extension View { + func scrollBounceBehaviorIfAvailable(_ behavior: BounceBehavior) -> some View { + if #available(iOS 16.4, *) { + return self.scrollBounceBehavior(behavior.behavior) + } else { + return self + } + } +} + +@available(iOS 13.0, *) +public extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} + +@available(iOS 13.0, *) +public struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + public func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +@available(iOS 13.0, *) +public struct ContentSizeModifier: ViewModifier { + @Binding var size: CGSize + + public func body(content: Content) -> some View { + content + .background( + GeometryReader { geometry -> Color in + if geometry.size != size { + DispatchQueue.main.async { + self.size = geometry.size + } + } + return Color.clear + } + ) + } +} + +@available(iOS 13.0, *) +public extension View { + func trackSize(_ size: Binding) -> some View { + self.modifier(ContentSizeModifier(size: size)) + } +} diff --git a/Swiftgram/SGTabBarHeightModifier/BUILD b/Swiftgram/SGTabBarHeightModifier/BUILD new file mode 100644 index 0000000000..6beaa48498 --- /dev/null +++ b/Swiftgram/SGTabBarHeightModifier/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGTabBarHeightModifier", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGTabBarHeightModifier/Sources/SGTabBarHeightModifier.swift b/Swiftgram/SGTabBarHeightModifier/Sources/SGTabBarHeightModifier.swift new file mode 100644 index 0000000000..8ff4cc57c1 --- /dev/null +++ b/Swiftgram/SGTabBarHeightModifier/Sources/SGTabBarHeightModifier.swift @@ -0,0 +1,26 @@ +import Foundation +import Display + +public func sgTabBarHeightModifier(showTabNames: Bool, tabBarHeight: CGFloat, layout: ContainerViewLayout, defaultBarSmaller: Bool) -> CGFloat { + var tabBarHeight = tabBarHeight + guard !showTabNames else { + return tabBarHeight + } + + if defaultBarSmaller { + tabBarHeight -= 6.0 + } else { + tabBarHeight -= 12.0 + } + + if layout.intrinsicInsets.bottom.isZero { + // Devices with home button need a bit more space + if defaultBarSmaller { + tabBarHeight += 3.0 + } else { + tabBarHeight += 6.0 + } + } + + return tabBarHeight +} diff --git a/Swiftgram/SGTranslationLangFix/BUILD b/Swiftgram/SGTranslationLangFix/BUILD new file mode 100644 index 0000000000..70f7354e97 --- /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 0000000000..f308de08df --- /dev/null +++ b/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift @@ -0,0 +1,9 @@ +public func sgTranslationLangFix(_ language: String) -> String { + if language.hasPrefix("de-") { + return "de" + } else 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 0000000000..1d581760f2 --- /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 0000000000..355a5664c2 --- /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 0000000000..ef1ee7626a --- /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 0000000000..e69de29bb2 diff --git a/Swiftgram/SGWebSettingsScheme/BUILD b/Swiftgram/SGWebSettingsScheme/BUILD new file mode 100644 index 0000000000..7bec107141 --- /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 0000000000..b8e976e99f --- /dev/null +++ b/Swiftgram/SGWebSettingsScheme/Sources/File.swift @@ -0,0 +1,55 @@ +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: "month", botMonkeys: [], forceReasons: [], unforceReasons: [], paymentsEnabled: true, duckyAppIconAvailable: true, canGrant: false, proSupportUrl: nil), user: SGUserSettings(contentReasons: [], canSendTelescope: false, canBuyInBeta: true)) + } +} + +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 let paymentsEnabled: Bool + public let duckyAppIconAvailable: Bool + public let canGrant: Bool + public let proSupportUrl: String? +} + +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 let canBuyInBeta: 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 0000000000..a4eeb901ea --- /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 0000000000..a3b329f03d --- /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 0000000000..2ffa006a80 --- /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 0000000000..fb98c57701 --- /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 0000000000..2cab2b56c7 --- /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 0000000000..d53c795072 --- /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 0000000000..7bb6feb592 --- /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 0000000000..fdeb0aebbe --- /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 0000000000..0892cad3fa --- /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 0000000000..7b309a53c5 --- /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 0000000000..c8129220e8 --- /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 0000000000..37f7199fa1 --- /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 0000000000..f2d0deec4e --- /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 0000000000..12e29cb514 --- /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 0000000000..95f9b10df3 --- /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 0000000000..630b9914bc --- /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 0000000000..b8e3852f12 --- /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 0000000000..b513301c27 --- /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 0000000000..0ecf21535e --- /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 0000000000..a4ab97ab94 --- /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 0000000000..a15754fa04 --- /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 0000000000..4f0fb9ec60 --- /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 0000000000..707248a83b --- /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 0000000000..a8b9ac0ede --- /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 0000000000..0ef99f0a42 --- /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 0000000000..0a52709895 --- /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 0000000000..2e6e864e56 --- /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 0000000000..c395170768 --- /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 0000000000..5f1032b6ab --- /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 0000000000..785a68b84c --- /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 0000000000..2a1b4a8578 --- /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 0000000000..055ab2b875 --- /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 f2f2f00f06..4d98fb99d7 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -142,6 +142,10 @@ genrule( "GeneratedPresentationStrings/Sources/PresentationStrings.m", "GeneratedPresentationStrings/Resources/PresentationStrings.data", ], + # MARK: Swiftgram + visibility = [ + "//visibility:public", + ], ) minimum_os_version = "12.0" @@ -254,16 +258,19 @@ filegroup( name = "AppStringResources", srcs = [ "Telegram-iOS/en.lproj/Localizable.strings", + "//Swiftgram/SGStrings:SGLocalizableStrings", ] + [ "{}.lproj/Localizable.strings".format(language) for language in empty_languages ], + # MARK: Swiftgram + visibility = ["//visibility:public",], ) filegroup( name = "WatchAppStringResources", srcs = glob([ "Telegram-iOS/*.lproj/Localizable.strings", - ], exclude = ["Telegram-iOS/*.lproj/**/.*"]), + ], exclude = ["Telegram-iOS/*.lproj/**/.*"]) + ["//Swiftgram/SGStrings:SGLocalizableStrings"], ) filegroup( @@ -322,19 +329,25 @@ 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", + "SGPro", + "SGGold", + "SGDucky", + "SGDay" ] [ @@ -360,12 +373,14 @@ objc_library( ], ) +SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets", "//Swiftgram/SGPayWall:SGPayWallAssets"] + swift_library( name = "Lib", srcs = glob([ "Telegram-iOS/Application.swift", ]), - data = [ + data = SGRESOURCES + [ ":Icons", ":AppResources", ":AppIntentVocabularyResources", @@ -426,6 +441,16 @@ plist_fragment( tonsite + + CFBundleTypeRole + Viewer + CFBundleURLName + {telegram_bundle_id}.custom + CFBundleURLSchemes + + sg + + """.format( telegram_bundle_id = telegram_bundle_id, @@ -512,6 +537,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 """ @@ -541,7 +567,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 @@ -551,6 +577,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", @@ -565,6 +598,7 @@ plist_fragment( carplay_fragment, communication_notifications_fragment, signin_fragment, + # content_analysis ]) ) @@ -603,6 +637,7 @@ objc_library( "Watch/SSignalKit/**/*.m", "Watch/Bridge/**/*.m", "Watch/WatchCommonWatch/**/*.m", + "Watch/App/**/*.m", "Watch/Extension/**/*.h", "Watch/SSignalKit/**/*.h", "Watch/Bridge/**/*.h", @@ -613,6 +648,7 @@ objc_library( "-ITelegram/Watch", "-ITelegram/Watch/Extension", "-ITelegram/Watch/Bridge", + "-ITelegram/Watch/App", ], sdk_frameworks = [ "WatchKit", @@ -652,30 +688,10 @@ plist_fragment( template = """ CFBundleDisplayName - Telegram + Swiftgram """ ) -plist_fragment( - name = "WatchExtensionNSExtensionInfoPlist", - extension = "plist", - template = - """ - NSExtension - - NSExtensionAttributes - - WKAppBundleIdentifier - {telegram_bundle_id}.watchkitapp - - NSExtensionPointIdentifier - com.apple.watchkit - - """.format( - telegram_bundle_id = telegram_bundle_id, - ) -) - plist_fragment( name = "WatchAppCompanionInfoPlist", extension = "plist", @@ -688,28 +704,6 @@ plist_fragment( ) ) -plist_fragment( - name = "WatchExtensionInfoPlist", - extension = "plist", - template = - """ - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - {telegram_bundle_id}.watchkitapp.watchkitextension - CFBundleName - Telegram - CFBundlePackageType - XPC! - WKExtensionDelegateClassName - TGExtensionDelegate - """.format( - telegram_bundle_id = telegram_bundle_id, - ) -) - plist_fragment( name = "WatchAppInfoPlist", extension = "plist", @@ -720,74 +714,31 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.watchkitapp CFBundleName - Telegram + Swiftgram UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - WKWatchKitApp - CFBundlePackageType APPL + WKApplication + + WKCompanionAppBundleIdentifier + {telegram_bundle_id} + PrincipalClass + TGExtensionDelegate """.format( telegram_bundle_id = telegram_bundle_id, ) ) -watchos_extension( - name = "TelegramWatchExtension", - bundle_id = "{telegram_bundle_id}.watchkitapp.watchkitextension".format( - telegram_bundle_id = telegram_bundle_id, - ), - bundle_name = "TelegramWatchExtension", - infoplists = [ - ":WatchExtensionInfoPlist", - ":VersionInfoPlist", - ":BuildNumberInfoPlist", - ":AppNameInfoPlist", - ":WatchExtensionNSExtensionInfoPlist", - ], - minimum_os_version = minimum_watchos_version, - provisioning_profile = select({ - ":disableProvisioningProfilesSetting": None, - "//conditions:default": "@build_configuration//provisioning:WatchExtension.mobileprovision", - }), - resources = [ - ":TelegramWatchExtensionResources", - ], - strings = [ - ":WatchAppStringResources", - ], - deps = [ - ":TelegramWatchLib", - ], -) - - -genrule( - name = "StripWatchosStubBinary", - cmd_bash = -""" - echo 'lipo -remove armv7k -remove arm64 -remove arm64e $$1/TelegramWatch.app/_WatchKitStub/WK -output $$1/TelegramWatch.app/_WatchKitStub/WK' > $(location StripWatchosStubBinary.sh) - echo '' >> $(location StripWatchosStubBinary.sh) -""", - outs = [ - "StripWatchosStubBinary.sh", - ], - executable = True, - visibility = [ - "//visibility:public", - ] -) - watchos_application( name = "TelegramWatchApp", bundle_id = "{telegram_bundle_id}.watchkitapp".format( telegram_bundle_id = telegram_bundle_id, ), - bundle_name = "TelegramWatch", - extension = ":TelegramWatchExtension", + bundle_name = "SwiftgramWatch", infoplists = [ ":WatchAppInfoPlist", ":VersionInfoPlist", @@ -800,15 +751,19 @@ watchos_application( ":disableProvisioningProfilesSetting": None, "//conditions:default": "@build_configuration//provisioning:WatchApp.mobileprovision", }), - ipa_post_processor = ":StripWatchosStubBinary", resources = [ ":TelegramWatchAppResources", ":TelegramWatchAppAssets", + ":TelegramWatchExtensionResources", ], storyboards = [ ":TelegramWatchAppInterface", ], strings = [ + ":WatchAppStringResources", + ], + deps = [ + ":TelegramWatchLib", ], ) @@ -1141,7 +1096,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Share CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1233,7 +1188,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationContent CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1340,7 +1295,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Widget CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1453,7 +1408,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.SiriIntents CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1574,6 +1529,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", @@ -1584,7 +1680,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.BroadcastUpload CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1678,7 +1774,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationService CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1746,11 +1842,11 @@ plist_fragment( CFBundleDevelopmentRegion en CFBundleDisplayName - Telegram + Swiftgram CFBundleIdentifier {telegram_bundle_id} CFBundleName - Telegram + Swiftgram CFBundlePackageType APPL CFBundleSignature @@ -1804,17 +1900,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 @@ -1921,7 +2017,7 @@ xcode_provisioning_profile( ) ios_application( - name = "Telegram", + name = "Swiftgram", bundle_id = "{telegram_bundle_id}".format( telegram_bundle_id = telegram_bundle_id, ), @@ -1958,9 +2054,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, @@ -1971,7 +2070,7 @@ ios_application( }), watch_application = select({ ":disableExtensionsSetting": None, - "//conditions:default": None#":TelegramWatchApp", + "//conditions:default": ":TelegramWatchApp", }) if telegram_enable_watch else None, deps = [ ":Main", @@ -1984,11 +2083,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/BUILD b/Telegram/NotificationService/BUILD index 6327b76b92..fa2ec92f8b 100644 --- a/Telegram/NotificationService/BUILD +++ b/Telegram/NotificationService/BUILD @@ -1,12 +1,16 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGAppGroupIdentifier:SGAppGroupIdentifier" +] + swift_library( name = "NotificationServiceExtensionLib", module_name = "NotificationServiceExtensionLib", srcs = glob([ "Sources/*.swift", ]), - deps = [ + deps = sgdeps + [ "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/BuildConfig:BuildConfig", diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 49eccbd317..c0613280d2 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1,3 +1,4 @@ +import SGAppGroupIdentifier import Foundation import UserNotifications import SwiftSignalKit @@ -18,6 +19,13 @@ import NotificationsPresentationData import RangeSet import ConvertOpusToAAC +private let groupUserDefaults: UserDefaults? = UserDefaults(suiteName: sgAppGroupIdentifier()) +private let LEGACY_NOTIFICATIONS_FIX: Bool = groupUserDefaults?.bool(forKey: "legacyNotificationsFix") ?? false +private let PINNED_MESSAGE_ACTION: String = groupUserDefaults?.string(forKey: "pinnedMessageNotifications") ?? "default" +private let PINNED_MESSAGE_ACTION_EXCEPTIONS: [String: String] = (groupUserDefaults?.dictionary(forKey: "pinnedMessageNotificationsExceptions") as? [String: String]) ?? [:] +private let MENTION_AND_REPLY_ACTION: String = groupUserDefaults?.string(forKey: "mentionsAndRepliesNotifications") ?? "default" +private let MENTION_AND_REPLY_ACTION_EXCEPTIONS: [String: String] = (groupUserDefaults?.dictionary(forKey: "mentionsAndRepliesNotificationsExceptions") as? [String: String]) ?? [:] + private let queue = Queue() private var installedSharedLogger = false @@ -496,14 +504,24 @@ private struct NotificationContent: CustomStringConvertible { var userInfo: [AnyHashable: Any] = [:] var attachments: [UNNotificationAttachment] = [] var silent = false + // MARK: Swiftgram + var isEmpty: Bool + var isMentionOrReply: Bool + var isPinned: Bool = false + let chatId: Int64? + let sgStatus: SGStatus var senderPerson: INPerson? var senderImage: INImage? var isLockedMessage: String? - init(isLockedMessage: String?) { + init(sgStatus: SGStatus, isLockedMessage: String?, isEmpty: Bool = false, isMentionOrReply: Bool = false, chatId: Int64? = nil) { + self.sgStatus = sgStatus self.isLockedMessage = isLockedMessage + self.isEmpty = isEmpty + self.isMentionOrReply = isMentionOrReply + self.chatId = chatId } var description: String { @@ -519,6 +537,13 @@ 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 += " chatId: \(String(describing: self.chatId)),\n" + string += " isMentionOrReply: \(self.isMentionOrReply),\n" + string += " isPinned: \(self.isPinned),\n" + string += " forceIsEmpty: \(self.forceIsEmpty),\n" + string += " forceIsSilent: \(self.forceIsSilent),\n" + string += " sgStatus: \(self.sgStatus.status),\n" string += "}" return string } @@ -533,7 +558,7 @@ private struct NotificationContent: CustomStringConvertible { if let topicTitle { displayName = "\(topicTitle) (\(displayName))" } - if self.silent { + if self.silent || self.forceIsSilent { displayName = "\(displayName) 🔕" } @@ -557,9 +582,15 @@ private struct NotificationContent: CustomStringConvertible { var content = UNMutableNotificationContent() //Logger.shared.log("NotificationService", "Generating final content: \(self.description)") - + // MARK: Swiftgram + #if DEBUG + print("body:\(content.body) silent:\(self.silent) isMentionOrReply:\(self.isMentionOrReply) MENTION_AND_REPLY_ACTION:\(MENTION_AND_REPLY_ACTION) isPinned:\(self.isPinned) PINNED_MESSAGE_ACTION:\(PINNED_MESSAGE_ACTION)" + " forceIsEmpty:\(self.forceIsEmpty) forceIsSilent:\(self.forceIsSilent)") + #endif + if self.forceIsEmpty && !LEGACY_NOTIFICATIONS_FIX { + return UNNotificationContent() + } if let title = self.title { - if self.silent { + if self.silent || self.forceIsSilent { content.title = "\(title) 🔕" } else { content.title = title @@ -638,7 +669,20 @@ private struct NotificationContent: CustomStringConvertible { } } } - + + // MARK: Swiftgram + if (self.isEmpty || self.forceIsEmpty) && LEGACY_NOTIFICATIONS_FIX { + content.title = " " + content.threadIdentifier = "empty-notification" + if #available(iOSApplicationExtension 15.0, iOS 15.0, *) { + content.interruptionLevel = .passive + content.relevanceScore = 0.0 + } + } + + if self.forceIsSilent { + content.sound = nil + } return content } } @@ -787,7 +831,8 @@ private final class NotificationServiceHandler { ApplicationSpecificSharedDataKeys.inAppNotificationSettings, ApplicationSpecificSharedDataKeys.voiceCallSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings, - SharedDataKeys.loggingSettings + SharedDataKeys.loggingSettings, + ApplicationSpecificSharedDataKeys.sgStatus ]) ) |> take(1) @@ -820,6 +865,7 @@ private final class NotificationServiceHandler { } let inAppNotificationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings + let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default let voiceCallSettings: VoiceCallSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) { @@ -831,7 +877,7 @@ private final class NotificationServiceHandler { guard let strongSelf = self, let recordId = recordId else { Logger.shared.log("NotificationService \(episode)", "Couldn't find a matching decryption key") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -853,7 +899,7 @@ private final class NotificationServiceHandler { guard let stateManager = stateManager else { Logger.shared.log("NotificationService \(episode)", "Didn't receive stateManager") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() return @@ -871,7 +917,7 @@ private final class NotificationServiceHandler { settings ) |> deliverOn(strongSelf.queue)).start(next: { notificationsKey, notificationSoundList in guard let strongSelf = self else { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -880,7 +926,7 @@ private final class NotificationServiceHandler { guard let notificationsKey = notificationsKey else { Logger.shared.log("NotificationService \(episode)", "Didn't receive decryption key") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -889,7 +935,7 @@ private final class NotificationServiceHandler { guard let decryptedPayload = decryptedNotificationPayload(key: notificationsKey, data: payloadData) else { Logger.shared.log("NotificationService \(episode)", "Couldn't decrypt payload") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -898,12 +944,17 @@ private final class NotificationServiceHandler { guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [String: Any] else { Logger.shared.log("NotificationService \(episode)", "Couldn't process payload as JSON") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() return } + let isMentionOrReply: Bool = payloadJson["mention"] as? String == "1" + var chatId: Int64? = nil + if let chatIdString = payloadJson["chat_id"] as? String { + chatId = Int64(chatIdString) + } Logger.shared.log("NotificationService \(episode)", "Decrypted payload: \(payloadJson)") @@ -1040,7 +1091,7 @@ private final class NotificationServiceHandler { action = .logout case "MESSAGE_MUTED": if let peerId = peerId { - action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil), messageId: nil, reportDelivery: false) + action = .poll(peerId: peerId, content: NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true, isMentionOrReply: isMentionOrReply, chatId: chatId), messageId: nil, reportDelivery: false) } case "MESSAGE_DELETED": if let peerId = peerId { @@ -1091,7 +1142,7 @@ private final class NotificationServiceHandler { } } else { if let aps = payloadJson["aps"] as? [String: Any], var peerId = peerId { - var content: NotificationContent = NotificationContent(isLockedMessage: isLockedMessage) + var content: NotificationContent = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage, isMentionOrReply: isMentionOrReply, chatId: chatId) if let alert = aps["alert"] as? [String: Any] { if let topicTitleValue = payloadJson["topic_title"] as? String { topicTitle = topicTitleValue @@ -1242,7 +1293,7 @@ private final class NotificationServiceHandler { switch action { case let .call(callData): if let stateManager = strongSelf.stateManager { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) let _ = (stateManager.postbox.transaction { transaction -> String? in @@ -1265,7 +1316,7 @@ private final class NotificationServiceHandler { if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration { Logger.shared.log("NotificationService \(episode)", "Will report voip notification") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in @@ -1274,7 +1325,7 @@ private final class NotificationServiceHandler { completed() }) } else { - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) if let peer = callData.peer { content.title = peer.debugDisplayTitle content.body = incomingCallMessage @@ -1335,7 +1386,7 @@ private final class NotificationServiceHandler { case .logout: Logger.shared.log("NotificationService \(episode)", "Will logout") - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true) updateCurrentContent(content) completed() case let .poll(peerId, initialContent, messageId, reportDelivery): @@ -1346,9 +1397,14 @@ private final class NotificationServiceHandler { let pollCompletion: (NotificationContent, Media?) -> Void = { content, customMedia in var content = content + // MARK: Swiftgram + if let mediaAction = customMedia as? TelegramMediaAction, case .pinnedMessageUpdated = mediaAction.action { + content.isPinned = true + } + queue.async { guard let strongSelf = self, let stateManager = strongSelf.stateManager else { - let content = NotificationContent(isLockedMessage: isLockedMessage) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage) updateCurrentContent(content) completed() return @@ -1654,7 +1710,7 @@ private final class NotificationServiceHandler { Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") if wasDisplayed { - content = NotificationContent(isLockedMessage: nil) + content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId) Logger.shared.log("NotificationService \(episode)", "Was already displayed, skipping content") } else if let messageId { let _ = (stateManager.postbox.transaction { transaction -> Void in @@ -1741,7 +1797,7 @@ private final class NotificationServiceHandler { case let .idBased(maxIncomingReadId, _, _, _, _): if maxIncomingReadId >= messageId.id { Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), skipping") - content = NotificationContent(isLockedMessage: nil) + content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId) } else { Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), not skipping") } @@ -1804,7 +1860,7 @@ private final class NotificationServiceHandler { queue.async { guard let strongSelf = self, let stateManager = strongSelf.stateManager else { - let content = NotificationContent(isLockedMessage: isLockedMessage) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage, isEmpty: true) updateCurrentContent(content) completed() return @@ -2004,7 +2060,7 @@ private final class NotificationServiceHandler { var content = content if wasDisplayed { - content = NotificationContent(isLockedMessage: nil) + content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) } else { let _ = (stateManager.postbox.transaction { transaction -> Void in _internal_setStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId)) @@ -2092,7 +2148,7 @@ private final class NotificationServiceHandler { postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true) if isCurrentAccount { content.badge = Int(value.0) } @@ -2134,7 +2190,7 @@ private final class NotificationServiceHandler { } let completeRemoval: () -> Void = { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true) Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)") updateCurrentContent(content) @@ -2186,7 +2242,7 @@ private final class NotificationServiceHandler { postbox: stateManager.postbox ) |> deliverOn(strongSelf.queue)).start(next: { value in - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true) if isCurrentAccount { content.badge = Int(value.0) } @@ -2227,7 +2283,7 @@ private final class NotificationServiceHandler { } let completeRemoval: () -> Void = { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true) updateCurrentContent(content) completed() @@ -2246,7 +2302,7 @@ private final class NotificationServiceHandler { }) } } else { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -2280,11 +2336,70 @@ 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() { + if !LEGACY_NOTIFICATIONS_FIX { + return + } + 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() { + if !LEGACY_NOTIFICATIONS_FIX { + return + } + 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 @@ -2315,7 +2430,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) } @@ -2342,3 +2462,53 @@ final class NotificationService: UNNotificationServiceExtension { } } } + + +extension NotificationContent { + var forceIsEmpty: Bool { + if self.sgStatus.status > 1 && !self.isEmpty { + if self.isPinned { + var desiredAction = PINNED_MESSAGE_ACTION + if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] { + desiredAction = exceptionAction + } + if desiredAction == "disabled" { + return true + } + } + if self.isMentionOrReply { + var desiredAction = MENTION_AND_REPLY_ACTION + if let chatId = chatId, let exceptionAction = MENTION_AND_REPLY_ACTION_EXCEPTIONS["\(chatId)"] { + desiredAction = exceptionAction + } + if desiredAction == "disabled" { + return true + } + } + } + return false + } + var forceIsSilent: Bool { + if self.sgStatus.status > 1 && !self.silent { + if self.isPinned { + var desiredAction = PINNED_MESSAGE_ACTION + if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] { + desiredAction = exceptionAction + } + if desiredAction == "silenced" { + return true + } + } + if self.isMentionOrReply { + var desiredAction = MENTION_AND_REPLY_ACTION + if let chatId = chatId, let exceptionAction = MENTION_AND_REPLY_ACTION_EXCEPTIONS["\(chatId)"] { + desiredAction = exceptionAction + } + if desiredAction == "silenced" { + return true + } + } + } + return false + } +} diff --git a/Telegram/SGActionRequestHandler/Action.js b/Telegram/SGActionRequestHandler/Action.js new file mode 100644 index 0000000000..11832ae69c --- /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 0000000000..73c00596a7 --- /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 0000000000..94a9fc2181 --- /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 0000000000..31ccdff021 --- /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 3364b2ef96..0000000000 --- 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 7169c854c3..0000000000 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 7169c854c3..0000000000 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 1529bf21a2..0000000000 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 90e1de1ecf..0000000000 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 d905a09233..0000000000 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 f7ed065d31..0000000000 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 20070867ec..0000000000 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 39eec67f83..0000000000 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 39eec67f83..0000000000 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 39eec67f83..0000000000 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 74aaa26f78..0000000000 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 74aaa26f78..0000000000 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 8d3559c8ff..0000000000 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 6314635155..0000000000 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 2948e25763..0000000000 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 2948e25763..0000000000 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 5176f7d26c..0000000000 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 8497b5a0d5..0000000000 --- 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 5a3a76cbdd..0000000000 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 5a3a76cbdd..0000000000 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 8044873c25..0000000000 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 bd9821af48..0000000000 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 a1d6016afb..0000000000 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 090c237445..0000000000 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 58f01e4c42..0000000000 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 fc834e964f..0000000000 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 fc834e964f..0000000000 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 fc834e964f..0000000000 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 e311513f49..0000000000 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 e311513f49..0000000000 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 d7e2100fda..0000000000 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 fb36db9eba..0000000000 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 b327187568..0000000000 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 b327187568..0000000000 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 7a1aec1271..0000000000 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 021eed91bf..0000000000 --- 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 9c5ca6a0cf..0000000000 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 9c5ca6a0cf..0000000000 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 de9fce9981..0000000000 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 fb761143f0..0000000000 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 a09fd70b81..0000000000 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 d5409e8ef1..0000000000 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 f9cf8bf695..0000000000 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 7004cb5a77..0000000000 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 7004cb5a77..0000000000 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 7004cb5a77..0000000000 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 8b5050f623..0000000000 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 8b5050f623..0000000000 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 dfea84b1b2..0000000000 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 dfba84e32f..0000000000 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 3f1f9d34ee..0000000000 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 3f1f9d34ee..0000000000 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 3c350f1649..0000000000 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 5315597fb0..0000000000 --- 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 9525324b1e..0000000000 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 9525324b1e..0000000000 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 d71dcd205e..0000000000 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 f51ae17df9..0000000000 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 facbf49ff3..0000000000 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 e865e6256b..0000000000 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 4865bb8b07..0000000000 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 e2b1ba7890..0000000000 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 e2b1ba7890..0000000000 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 e2b1ba7890..0000000000 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 b9f52c5932..0000000000 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 b9f52c5932..0000000000 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 ffae9ee7b7..0000000000 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 07de560340..0000000000 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 8d4fe9efe6..0000000000 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 8d4fe9efe6..0000000000 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 95b278c284..0000000000 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 da4a164c91..0000000000 --- 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 093f5821a5..0000000000 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 13f8fe2694..0000000000 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 46593ec465..0000000000 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 ed0216f931..0000000000 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 1fcc6fc9bb..0000000000 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 20fe7d5eef..0000000000 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 fafc0e385e..0000000000 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 f00e3e2d61..0000000000 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 a327546043..0000000000 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 a3972adeca..0000000000 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 d86fb7cb55..0000000000 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 0b52118b1c..0000000000 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 90e1de1ecf..0000000000 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 5a3a76cbdd..0000000000 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 a1d6016afb..0000000000 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 fb36db9eba..0000000000 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 8044873c25..0000000000 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 bd9821af48..0000000000 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 55ae148ed8..0000000000 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 638b30f339..0000000000 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 8b28ed057b..0000000000 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 aa3ec282ce..0000000000 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 eca037efcf..0000000000 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 2e5e919205..0000000000 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 08da0b799a..0000000000 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 342e2766d9..0000000000 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 b8befb1c7b..0000000000 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 aa84350de5..0000000000 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 32683874aa..0000000000 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 7c851299aa..0000000000 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 49b7fd968c..0000000000 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 dfba84e32f..0000000000 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 de9fce9981..0000000000 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 fb761143f0..0000000000 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 2e502e7dab..0000000000 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 c47aeed4b1..0000000000 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 f07ad9568b..0000000000 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 1b21e8d928..0000000000 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 9bf363744d..0000000000 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 dc5916282e..0000000000 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 0898af42d9..0000000000 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 f7725e9914..0000000000 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 dd360d8f50..0000000000 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 2e502e7dab..0000000000 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 c47aeed4b1..0000000000 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 6d9e7ab98c..0000000000 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 9bf363744d..0000000000 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 dc5916282e..0000000000 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 0898af42d9..0000000000 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 0898af42d9..0000000000 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 f7725e9914..0000000000 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 4d65457087..b45cfedbdc 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 f00a2857f0..0000000000 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 90d7b67bc0..0000000000 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 a79cb5dcdc..0000000000 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 aa6a4a442e..0000000000 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 aa6a4a442e..0000000000 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 385bc474b2..0000000000 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 385bc474b2..0000000000 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 c0a9ce9319..0000000000 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 0000000000..a28a393d1e 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 da4a164c91..73c00596a7 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 9525324b1e..0000000000 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 facbf49ff3..0000000000 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 07de560340..0000000000 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 d71dcd205e..0000000000 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 f51ae17df9..0000000000 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 e2b1ba7890..0000000000 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 8d4fe9efe6..0000000000 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 9525324b1e..0000000000 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 4865bb8b07..0000000000 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 b9f52c5932..0000000000 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 95b278c284..0000000000 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 c85f9bc45a..0000000000 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 32adc011d1..0000000000 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 93238e0c7f..0000000000 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 70ddc32cbe..0000000000 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 ced492fd40..0000000000 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 6387cb01bd..0000000000 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 93c34d10c7..0000000000 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 fb4f4a6122..0000000000 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 1b3e74dfa6..0000000000 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 34afc4fbec..0000000000 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 e29005ac4e..0000000000 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 54a04f2c27..0000000000 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 ca043ed339..0000000000 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 d94dc86c61..0000000000 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 813a39a5bd..0000000000 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 fe2d70eada..0000000000 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 6750299df3..0000000000 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 4e0265fa69..0000000000 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 37e2e15d18..0000000000 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 1815681047..0000000000 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 85de9e3fab..0000000000 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 a083c562d4..0000000000 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 d2d9d52e92..0000000000 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 caab8e9d8a..0000000000 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 00ea76d714..0000000000 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 1a67519593..0000000000 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 cb953d3546..0000000000 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 a3833ef0c7..0000000000 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 7eccb509ee..0000000000 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 a243d72e63..0000000000 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 7a5a342bc9..7260909f91 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 826d68b263..5bb5b80fc8 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 0000000000..c5c51e0b71 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 0000000000..c2c05b4192 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 0000000000..9ece7f08aa 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 0000000000..532041e0c3 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png differ diff --git a/Telegram/Telegram-iOS/SGDay.alticon/SGDay@2x.png b/Telegram/Telegram-iOS/SGDay.alticon/SGDay@2x.png new file mode 100644 index 0000000000..2532eb5f8e Binary files /dev/null and b/Telegram/Telegram-iOS/SGDay.alticon/SGDay@2x.png differ diff --git a/Telegram/Telegram-iOS/SGDay.alticon/SGDay@3x.png b/Telegram/Telegram-iOS/SGDay.alticon/SGDay@3x.png new file mode 100644 index 0000000000..f368216d0c Binary files /dev/null and b/Telegram/Telegram-iOS/SGDay.alticon/SGDay@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 0000000000..02c8cbb05c 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 0000000000..6485f58918 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png differ diff --git a/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png b/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png new file mode 100644 index 0000000000..7a4ee59e65 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png differ diff --git a/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@3x.png b/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@3x.png new file mode 100644 index 0000000000..1ec818f673 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@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 0000000000..a70a819abd 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 0000000000..43a38972b7 Binary files /dev/null and b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png differ diff --git a/Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png b/Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png new file mode 100644 index 0000000000..1e929251b5 Binary files /dev/null and b/Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png differ diff --git a/Telegram/Telegram-iOS/SGGold.alticon/SGGold@3x.png b/Telegram/Telegram-iOS/SGGold.alticon/SGGold@3x.png new file mode 100644 index 0000000000..38c0118975 Binary files /dev/null and b/Telegram/Telegram-iOS/SGGold.alticon/SGGold@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 0000000000..f8131ff517 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 0000000000..e1fc51be8f 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 0000000000..bc4426140f 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 0000000000..f6e25e84cd 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 0000000000..1e6dd6862e 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 0000000000..ff12511d04 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 0000000000..191c60f764 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 0000000000..cc37ad00f2 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 0000000000..df54b8cd97 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 0000000000..2c238b101a Binary files /dev/null and b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png differ diff --git a/Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png b/Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png new file mode 100644 index 0000000000..bdeaaac60f Binary files /dev/null and b/Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png differ diff --git a/Telegram/Telegram-iOS/SGPro.alticon/SGPro@3x.png b/Telegram/Telegram-iOS/SGPro.alticon/SGPro@3x.png new file mode 100644 index 0000000000..30464e4635 Binary files /dev/null and b/Telegram/Telegram-iOS/SGPro.alticon/SGPro@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 0000000000..94d7ec24a0 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 0000000000..d4ac553a2d 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 0000000000..5cadd1d556 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 0000000000..c5bfeb8f1e 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 0000000000..e6a99cf622 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 0000000000..2e2b2bc6c3 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 0000000000..3f9a26322d 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 0000000000..b6070b75de 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 2e52591bc3..0000000000 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 fb138b5228..0000000000 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 0a71b7adba..fcdf65836c 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 174276a904..f52a6d14f9 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 504ece4483..136ad1e3db 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 504ece4483..136ad1e3db 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 3956121060..25217fc93b 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 504ece4483..136ad1e3db 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 ca33866340..9f47a12484 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 335936cef8..d16324e1f7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -401,23 +401,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"; @@ -432,7 +432,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."; @@ -443,13 +443,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"; @@ -505,8 +505,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"; @@ -544,7 +544,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..."; @@ -848,9 +848,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"; @@ -1218,7 +1218,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"; @@ -1276,32 +1276,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"; @@ -1439,7 +1439,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"; @@ -1475,7 +1475,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"; @@ -1585,7 +1585,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."; @@ -1882,7 +1882,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."; @@ -2297,11 +2297,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."; @@ -2351,7 +2351,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"; @@ -2538,14 +2538,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"; @@ -2610,7 +2610,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."; @@ -2620,8 +2620,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"; @@ -2629,8 +2629,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"; @@ -2656,7 +2656,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"; @@ -2807,7 +2807,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"; @@ -2874,7 +2874,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"; @@ -2906,14 +2906,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"; @@ -3069,12 +3069,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"; @@ -3157,16 +3157,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"; @@ -3310,7 +3310,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"; @@ -3588,9 +3588,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"; @@ -3781,8 +3781,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]"; @@ -3869,7 +3869,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"; @@ -3880,7 +3880,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"; @@ -3911,7 +3911,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"; @@ -3922,7 +3922,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?"; @@ -3995,8 +3995,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."; @@ -4133,7 +4133,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"; @@ -4439,7 +4439,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."; @@ -5062,11 +5062,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"; @@ -5687,7 +5687,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."; @@ -5841,7 +5841,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"; @@ -6185,7 +6185,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."; @@ -6337,7 +6337,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."; @@ -6643,7 +6643,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"; @@ -6944,7 +6944,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"; @@ -7310,8 +7310,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"; @@ -7424,8 +7424,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"; @@ -7469,8 +7469,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"; @@ -7547,7 +7547,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."; @@ -7559,7 +7559,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..."; @@ -7647,7 +7647,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"; @@ -8070,7 +8070,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"; @@ -8630,7 +8630,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"; @@ -9036,7 +9036,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."; @@ -9397,7 +9397,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"; @@ -9566,8 +9566,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."; @@ -9642,7 +9642,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"; @@ -9948,7 +9948,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 fd11102f14..ae7044bbbc 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 504ece4483..136ad1e3db 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 504ece4483..136ad1e3db 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 504ece4483..136ad1e3db 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 8710a6c624..550ebf8179 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 932e5f6d28..980b2b7448 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 504ece4483..136ad1e3db 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 5c709b84ee..fcc0a842f1 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 504ece4483..136ad1e3db 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 018471dbc6..2e33f8946e 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 72fd63d738..643154cf8c 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 504ece4483..136ad1e3db 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 504ece4483..136ad1e3db 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 504ece4483..136ad1e3db 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/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json index b46b3e5abb..f1ab5a7e38 100644 --- a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,92 +1,14 @@ { "images" : [ { - "size" : "24x24", - "idiom" : "watch", - "filename" : "Watch48@2x.png", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "38mm" - }, - { - "size" : "27.5x27.5", - "idiom" : "watch", - "filename" : "Watch55@2x.png", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "42mm" - }, - { - "size" : "29x29", - "idiom" : "watch", - "filename" : "Simple@58x58.png", - "role" : "companionSettings", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "watch", - "filename" : "Simple@87x87.png", - "role" : "companionSettings", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "watch", - "filename" : "Simple@80x80.png", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "38mm" - }, - { - "size" : "44x44", - "idiom" : "watch", - "filename" : "Watch88@2x.png", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "40mm" - }, - { - "size" : "50x50", - "idiom" : "watch", - "filename" : "Watch100@2x.png", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "44mm" - }, - { - "size" : "86x86", - "idiom" : "watch", - "filename" : "Watch172@2x.png", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "38mm" - }, - { - "size" : "98x98", - "idiom" : "watch", - "filename" : "Watch196@2x.png", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "42mm" - }, - { - "size" : "108x108", - "idiom" : "watch", - "filename" : "Watch216@2x.png", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "44mm" - }, - { - "size" : "1024x1024", - "idiom" : "watch-marketing", - "filename" : "Simple-iTunesArtwork.png", - "scale" : "1x" + "filename" : "Swiftgram.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple-iTunesArtwork.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple-iTunesArtwork.png deleted file mode 100644 index a927ff5a6b..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple-iTunesArtwork.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@58x58.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@58x58.png deleted file mode 100644 index 7559ff4a34..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@58x58.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@80x80.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@80x80.png deleted file mode 100644 index a5723c6dc4..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@80x80.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@87x87.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@87x87.png deleted file mode 100644 index ee652ff0a2..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Simple@87x87.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Swiftgram.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Swiftgram.png new file mode 100644 index 0000000000..a28a393d1e Binary files /dev/null and b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Swiftgram.png differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch100@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch100@2x.png deleted file mode 100644 index aa69bfd47b..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch100@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch172@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch172@2x.png deleted file mode 100644 index a0b41dc3e5..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch172@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch196@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch196@2x.png deleted file mode 100644 index 0b6d014c08..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch196@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch216@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch216@2x.png deleted file mode 100644 index 771bd56e60..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch216@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch48@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch48@2x.png deleted file mode 100644 index f81b3a85a6..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch48@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch55@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch55@2x.png deleted file mode 100644 index 5838cb2620..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch55@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch88@2x.png b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch88@2x.png deleted file mode 100644 index 71fb4bbf9c..0000000000 Binary files a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Watch88@2x.png and /dev/null differ diff --git a/Telegram/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png b/Telegram/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png index b2ecb14b80..0373f2cf07 100644 Binary files a/Telegram/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png and b/Telegram/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png differ diff --git a/Telegram/Watch/App/Base.lproj/Interface.storyboard b/Telegram/Watch/App/Base.lproj/Interface.storyboard index 35d1335cea..5ed5d2c6e1 100644 --- a/Telegram/Watch/App/Base.lproj/Interface.storyboard +++ b/Telegram/Watch/App/Base.lproj/Interface.storyboard @@ -1094,10 +1094,10 @@ contacts found. - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Telegram/Watch/App/main.m b/Telegram/Watch/App/main.m new file mode 100644 index 0000000000..d0ed309f53 --- /dev/null +++ b/Telegram/Watch/App/main.m @@ -0,0 +1,8 @@ +#import +#import "TGExtensionDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return WKApplicationMain(argc, argv, @"TGExtensionDelegate"); + } +} diff --git a/Telegram/Watch/Bridge/TGBridgeBotReplyMarkup.h b/Telegram/Watch/Bridge/TGBridgeBotReplyMarkup.h index f0cf962cde..0928737399 100644 --- a/Telegram/Watch/Bridge/TGBridgeBotReplyMarkup.h +++ b/Telegram/Watch/Bridge/TGBridgeBotReplyMarkup.h @@ -28,7 +28,7 @@ @interface TGBridgeBotReplyMarkup : NSObject { - int32_t _userId; + int64_t _userId; int32_t _messageId; TGBridgeMessage *_message; bool _hideKeyboardOnActivation; @@ -36,7 +36,7 @@ NSArray *_rows; } -@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, readonly) int64_t userId; @property (nonatomic, readonly) int32_t messageId; @property (nonatomic, readonly) TGBridgeMessage *message; @property (nonatomic, readonly) bool hideKeyboardOnActivation; diff --git a/Telegram/Watch/Bridge/TGBridgeBotSignals.h b/Telegram/Watch/Bridge/TGBridgeBotSignals.h index a3930248fa..4e4151e287 100644 --- a/Telegram/Watch/Bridge/TGBridgeBotSignals.h +++ b/Telegram/Watch/Bridge/TGBridgeBotSignals.h @@ -2,7 +2,7 @@ @interface TGBridgeBotSignals : NSObject -+ (SSignal *)botInfoForUserId:(int32_t)userId; ++ (SSignal *)botInfoForUserId:(int64_t)userId; + (SSignal *)botReplyMarkupForPeerId:(int64_t)peerId; @end diff --git a/Telegram/Watch/Bridge/TGBridgeBotSignals.m b/Telegram/Watch/Bridge/TGBridgeBotSignals.m index d1abf20b67..62dbbfe014 100644 --- a/Telegram/Watch/Bridge/TGBridgeBotSignals.m +++ b/Telegram/Watch/Bridge/TGBridgeBotSignals.m @@ -8,7 +8,7 @@ @implementation TGBridgeBotSignals -+ (SSignal *)botInfoForUserId:(int32_t)userId ++ (SSignal *)botInfoForUserId:(int64_t)userId { SSignal *cachedSignal = [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) { diff --git a/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.h b/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.h index 0a4fe21387..b987136791 100644 --- a/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.h +++ b/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.h @@ -2,7 +2,7 @@ @interface TGBridgeUserInfoSignals : NSObject -+ (SSignal *)userInfoWithUserId:(int32_t)userId; ++ (SSignal *)userInfoWithUserId:(int64_t)userId; + (SSignal *)usersInfoWithUserIds:(NSArray *)userIds; @end diff --git a/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.m b/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.m index bdd2978190..19c22f5141 100644 --- a/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.m +++ b/Telegram/Watch/Bridge/TGBridgeUserInfoSignals.m @@ -6,7 +6,7 @@ @implementation TGBridgeUserInfoSignals -+ (SSignal *)userInfoWithUserId:(int32_t)userId; ++ (SSignal *)userInfoWithUserId:(int64_t)userId; { return [[self usersInfoWithUserIds:@[ @(userId) ]] map:^TGBridgeUser *(NSDictionary *users) { diff --git a/Telegram/Watch/Extension/TGAvatarViewModel.m b/Telegram/Watch/Extension/TGAvatarViewModel.m index a841af8629..98e24eec5f 100644 --- a/Telegram/Watch/Extension/TGAvatarViewModel.m +++ b/Telegram/Watch/Extension/TGAvatarViewModel.m @@ -56,7 +56,7 @@ { self.label.hidden = false; self.label.text = [TGStringUtils initialsForFirstName:_currentUser.firstName lastName:_currentUser.lastName single:true]; - self.group.backgroundColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:context.userId]; + self.group.backgroundColor = [TGColor colorForUserId:(int64_t)user.identifier myUserId:context.userId]; } } } diff --git a/Telegram/Watch/Extension/TGBridgeUserCache.h b/Telegram/Watch/Extension/TGBridgeUserCache.h index b48906356f..0a345cca35 100644 --- a/Telegram/Watch/Extension/TGBridgeUserCache.h +++ b/Telegram/Watch/Extension/TGBridgeUserCache.h @@ -11,8 +11,8 @@ - (void)storeUsers:(NSArray *)users; - (NSArray *)applyUserChanges:(NSArray *)userChanges; -- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId; -- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId; +- (TGBridgeBotInfo *)botInfoForUserId:(int64_t)userId; +- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int64_t)userId; + (instancetype)instance; diff --git a/Telegram/Watch/Extension/TGBridgeUserCache.m b/Telegram/Watch/Extension/TGBridgeUserCache.m index 92a77bc055..9b407827b1 100644 --- a/Telegram/Watch/Extension/TGBridgeUserCache.m +++ b/Telegram/Watch/Extension/TGBridgeUserCache.m @@ -115,7 +115,7 @@ return missedUserIds; } -- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId +- (TGBridgeBotInfo *)botInfoForUserId:(int64_t)userId { __block TGBridgeBotInfo *botInfo = nil; @@ -147,7 +147,7 @@ return botInfo; } -- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId +- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int64_t)userId { OSSpinLockLock(&_botInfoByUidLock); _botInfoByUid[@(userId)] = botInfo; diff --git a/Telegram/Watch/Extension/TGGroupInfoController.m b/Telegram/Watch/Extension/TGGroupInfoController.m index 77117a8f45..f9e367d504 100644 --- a/Telegram/Watch/Extension/TGGroupInfoController.m +++ b/Telegram/Watch/Extension/TGGroupInfoController.m @@ -279,7 +279,7 @@ NSString *const TGGroupInfoControllerIdentifier = @"TGGroupInfoController"; return [[TGUserInfoControllerContext alloc] initWithUser:_currentParticipantsModels[indexPath.row]]; } -+ (NSMutableArray *)sortedParticipantsList:(NSMutableArray *)list preferredOrder:(NSDictionary *)preferredOrder ownUid:(int32_t)ownUid ++ (NSMutableArray *)sortedParticipantsList:(NSMutableArray *)list preferredOrder:(NSDictionary *)preferredOrder ownUid:(int64_t)ownUid { NSMutableArray *resultList = [list mutableCopy]; diff --git a/Telegram/Watch/Extension/TGMessageViewController.m b/Telegram/Watch/Extension/TGMessageViewController.m index f6929f7b37..ee47fad8c3 100644 --- a/Telegram/Watch/Extension/TGMessageViewController.m +++ b/Telegram/Watch/Extension/TGMessageViewController.m @@ -158,7 +158,7 @@ NSString *const TGMessageViewControllerIdentifier = @"TGMessageViewController"; } else { - TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)_context.message.fromUid]; + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int64_t)_context.message.fromUid]; [controller updateWithUser:user context:_context.context]; } } @@ -173,7 +173,7 @@ NSString *const TGMessageViewControllerIdentifier = @"TGMessageViewController"; } else { - TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)_context.message.fromUid]; + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int64_t)_context.message.fromUid]; [self pushControllerWithClass:[TGUserInfoController class] context:context]; } } @@ -280,7 +280,7 @@ NSString *const TGMessageViewControllerIdentifier = @"TGMessageViewController"; if (TGPeerIdIsChannel(peerId)) context = [[TGUserInfoControllerContext alloc] initWithChannel:_context.additionalPeers[@(peerId)]]; else - context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)peerId]; + context = [[TGUserInfoControllerContext alloc] initWithUserId:(int64_t)peerId]; [strongSelf pushControllerWithClass:[TGUserInfoController class] context:context]; } diff --git a/Telegram/Watch/Extension/TGMessageViewMessageRowController.m b/Telegram/Watch/Extension/TGMessageViewMessageRowController.m index a377a119cc..20d893b99b 100644 --- a/Telegram/Watch/Extension/TGMessageViewMessageRowController.m +++ b/Telegram/Watch/Extension/TGMessageViewMessageRowController.m @@ -234,7 +234,7 @@ NSString *const TGMessageViewMessageRowIdentifier = @"TGMessageViewMessageRow"; else { self.avatarInitialsLabel.hidden = false; - self.avatarGroup.backgroundColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:context.userId]; + self.avatarGroup.backgroundColor = [TGColor colorForUserId:(int64_t)user.identifier myUserId:context.userId]; self.avatarInitialsLabel.text = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:true]; [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; @@ -280,7 +280,7 @@ NSString *const TGMessageViewMessageRowIdentifier = @"TGMessageViewMessageRow"; if (TGPeerIdIsChannel(forwardAttachment.peerId)) forwardPeer = additionalPeers[@(forwardAttachment.peerId)]; else - forwardPeer = [[TGBridgeUserCache instance] userWithId:(int32_t)forwardAttachment.peerId]; + forwardPeer = [[TGBridgeUserCache instance] userWithId:(int64_t)forwardAttachment.peerId]; } [TGMessageViewModel updateForwardHeaderGroup:self.forwardHeaderButton titleLabel:self.forwardTitleLabel fromLabel:self.forwardFromLabel forwardAttachment:forwardAttachment forwardPeer:forwardPeer textColor:[UIColor whiteColor]]; diff --git a/Telegram/Watch/Extension/TGMessageViewModel.h b/Telegram/Watch/Extension/TGMessageViewModel.h index 7feef050c4..1ee1bce026 100644 --- a/Telegram/Watch/Extension/TGMessageViewModel.h +++ b/Telegram/Watch/Extension/TGMessageViewModel.h @@ -10,7 +10,7 @@ @interface TGMessageViewModel : NSObject -+ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int32_t)ownUserId; ++ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int64_t)ownUserId; + (void)updateMediaGroup:(WKInterfaceGroup *)mediaGroup activityIndicator:(WKInterfaceImage *)activityIndicator attachment:(TGBridgeMediaAttachment *)mediaAttachment message:(TGBridgeMessage *)message notification:(bool)notification currentPhoto:(int64_t *)currentPhoto standalone:(bool)standalone margin:(CGFloat)margin imageSize:(CGSize *)imageSize isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion; diff --git a/Telegram/Watch/Extension/TGMessageViewModel.m b/Telegram/Watch/Extension/TGMessageViewModel.m index 052cba63ec..01f84bc415 100644 --- a/Telegram/Watch/Extension/TGMessageViewModel.m +++ b/Telegram/Watch/Extension/TGMessageViewModel.m @@ -58,13 +58,13 @@ *thumbnailSize = imageSize; } -+ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int32_t)ownUserId ++ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int64_t)ownUserId { if (isGroup && !isOutgoing) { authorLabel.hidden = false; authorLabel.text = user.displayName; - authorLabel.textColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:ownUserId]; + authorLabel.textColor = [TGColor colorForUserId:(int64_t)user.identifier myUserId:ownUserId]; } else { @@ -268,7 +268,7 @@ textColor = subtitleColor; } - authorLabel.text = [[[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid] displayName]; + authorLabel.text = [[[TGBridgeUserCache instance] userWithId:(int64_t)message.fromUid] displayName]; imageGroup.hidden = !hasImagePreview; textLabel.text = messageText; textLabel.textColor = textColor; @@ -325,7 +325,7 @@ + (NSString *)stringForActionAttachment:(TGBridgeActionMediaAttachment *)actionAttachment message:(TGBridgeMessage *)message users:(NSDictionary *)users forChannel:(bool)forChannel { NSString *messageText = nil; - TGBridgeUser *author = (users != nil) ? users[@(message.fromUid)] : [[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid]; + TGBridgeUser *author = (users != nil) ? users[@(message.fromUid)] : [[TGBridgeUserCache instance] userWithId:(int64_t)message.fromUid]; switch (actionAttachment.actionType) { @@ -373,7 +373,7 @@ case TGBridgeMessageActionChatDeleteMember: { NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; - TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int32Value]]; + TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int64Value]]; if (user.identifier == author.identifier) { @@ -419,7 +419,7 @@ case TGBridgeMessageActionChannelInviter: { - TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int32Value]]; + TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int64Value]]; NSString *authorName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); @@ -486,7 +486,7 @@ + (void)updateWithMessage:(TGBridgeMessage *)message notification:(bool)notification isGroup:(bool)isGroup context:(TGBridgeContext *)context currentDocumentId:(int64_t *)currentDocumentId authorLabel:(WKInterfaceLabel *)authorLabel imageGroup:(WKInterfaceGroup *)imageGroup isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion { - [TGMessageViewModel updateAuthorLabel:authorLabel isOutgoing:message.outgoing isGroup:isGroup user:[[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid] ownUserId:context.userId]; + [TGMessageViewModel updateAuthorLabel:authorLabel isOutgoing:message.outgoing isGroup:isGroup user:[[TGBridgeUserCache instance] userWithId:(int64_t)message.fromUid] ownUserId:context.userId]; for (TGBridgeMediaAttachment *attachment in message.media) { diff --git a/Telegram/Watch/Extension/TGNeoAttachmentViewModel.m b/Telegram/Watch/Extension/TGNeoAttachmentViewModel.m index 5829e8a35f..de63982092 100644 --- a/Telegram/Watch/Extension/TGNeoAttachmentViewModel.m +++ b/Telegram/Watch/Extension/TGNeoAttachmentViewModel.m @@ -198,7 +198,7 @@ case TGBridgeMessageActionChatDeleteMember: { NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; - TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int64Value])]; if (user.identifier == author.identifier) { @@ -276,7 +276,7 @@ case TGBridgeMessageActionChannelInviter: { - TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int64Value])]; NSString *authorName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); diff --git a/Telegram/Watch/Extension/TGNeoBubbleMessageViewModel.m b/Telegram/Watch/Extension/TGNeoBubbleMessageViewModel.m index fa07d55b9d..ca43ceab6a 100644 --- a/Telegram/Watch/Extension/TGNeoBubbleMessageViewModel.m +++ b/Telegram/Watch/Extension/TGNeoBubbleMessageViewModel.m @@ -28,7 +28,7 @@ const CGFloat TGNeoBubbleHeaderSpacing = 2.0f; if (!message.outgoing && type == TGNeoMessageTypeGroup) { - _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int64_t)message.fromUid myUserId:context.userId] attributes:nil]; [self addSubmodel:_authorNameModel]; } diff --git a/Telegram/Watch/Extension/TGNeoChatsController.m b/Telegram/Watch/Extension/TGNeoChatsController.m index 9a546a3762..aa3ce5a7a7 100644 --- a/Telegram/Watch/Extension/TGNeoChatsController.m +++ b/Telegram/Watch/Extension/TGNeoChatsController.m @@ -28,9 +28,9 @@ NSString *const TGContextNotificationKey = @"context"; NSString *const TGSynchronizationStateNotification = @"TGSynchronizationStateNotification"; NSString *const TGSynchronizationStateKey = @"state"; -const NSUInteger TGNeoChatsControllerInitialCount = 3; -const NSUInteger TGNeoChatsControllerLimit = 12; -const NSUInteger TGNeoChatsControllerForwardLimit = 20; +const NSUInteger TGNeoChatsControllerInitialCount = 4; +const NSUInteger TGNeoChatsControllerLimit = 12 * 2; +const NSUInteger TGNeoChatsControllerForwardLimit = 20 * 2; @implementation TGNeoChatsControllerContext diff --git a/Telegram/Watch/Extension/TGNeoContactMessageViewModel.m b/Telegram/Watch/Extension/TGNeoContactMessageViewModel.m index a67c90e6b8..3dd62681dc 100644 --- a/Telegram/Watch/Extension/TGNeoContactMessageViewModel.m +++ b/Telegram/Watch/Extension/TGNeoContactMessageViewModel.m @@ -12,8 +12,8 @@ TGNeoLabelViewModel *_nameModel; TGNeoLabelViewModel *_phoneModel; - int32_t _userId; - int32_t _ownUserId; + int64_t _userId; + int64_t _ownUserId; NSString *_avatarUrl; NSString *_firstName; NSString *_lastName; diff --git a/Telegram/Watch/Extension/TGNeoConversationController.m b/Telegram/Watch/Extension/TGNeoConversationController.m index a40861f6d5..b18ee78a17 100644 --- a/Telegram/Watch/Extension/TGNeoConversationController.m +++ b/Telegram/Watch/Extension/TGNeoConversationController.m @@ -44,11 +44,11 @@ #import "TGAudioMicAlertController.h" NSString *const TGNeoConversationControllerIdentifier = @"TGNeoConversationController"; -const NSInteger TGNeoConversationControllerDefaultBatchLimit = 8; -const NSInteger TGNeoConversationControllerPerformantBatchLimit = 10; -const NSInteger TGNeoConversationControllerMaximumBatchLimit = 20; +const NSInteger TGNeoConversationControllerDefaultBatchLimit = 8 * 2; +const NSInteger TGNeoConversationControllerPerformantBatchLimit = 10 * 2; +const NSInteger TGNeoConversationControllerMaximumBatchLimit = 20 * 2; -const NSInteger TGNeoConversationControllerInitialRenderCount = 4; +const NSInteger TGNeoConversationControllerInitialRenderCount = 4 * 2; @interface TGNeoConversationControllerContext () { @@ -513,7 +513,7 @@ const NSInteger TGNeoConversationControllerInitialRenderCount = 4; else if (_context.context.userId == _context.peerId) return TGLocalized(@"Conversation.SavedMessages"); else - return [[[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]] displayName]; + return [[[TGBridgeUserCache instance] userWithId:(int64_t)[self peerId]] displayName]; } - (void)configureHandoff @@ -582,7 +582,7 @@ const NSInteger TGNeoConversationControllerInitialRenderCount = 4; } else { - TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)[strongSelf peerId]]; + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int64_t)[strongSelf peerId]]; context.disallowCompose = true; [controller pushControllerWithClass:[TGUserInfoController class] context:context]; } @@ -736,7 +736,7 @@ const NSInteger TGNeoConversationControllerInitialRenderCount = 4; } else if ([self _userIsBot]) { - int32_t userId = (int32_t)[self peerId]; + int64_t userId = (int64_t)[self peerId]; return [[TGBridgeBotSignals botInfoForUserId:userId] map:^NSArray *(TGBridgeBotInfo *botInfo) { if (botInfo != nil) @@ -757,7 +757,7 @@ const NSInteger TGNeoConversationControllerInitialRenderCount = 4; if ([self peerId] < 0) return false; - TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int64_t)[self peerId]]; return [user isBot]; } @@ -779,7 +779,7 @@ const NSInteger TGNeoConversationControllerInitialRenderCount = 4; } else { - TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int64_t)[self peerId]]; _hasBots = [user isBot]; } } diff --git a/Telegram/Watch/Extension/TGNeoServiceMessageViewModel.m b/Telegram/Watch/Extension/TGNeoServiceMessageViewModel.m index 2fa384c081..74e457b64f 100644 --- a/Telegram/Watch/Extension/TGNeoServiceMessageViewModel.m +++ b/Telegram/Watch/Extension/TGNeoServiceMessageViewModel.m @@ -94,7 +94,7 @@ const UIEdgeInsets TGNeoChatInfoInsets = { 12, 0, 12, 0 }; case TGBridgeMessageActionChatDeleteMember: { NSString *authorName = author.displayName; - TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int64Value])]; if (user.identifier == author.identifier) { @@ -172,7 +172,7 @@ const UIEdgeInsets TGNeoChatInfoInsets = { 12, 0, 12, 0 }; case TGBridgeMessageActionChannelInviter: { - TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int64Value])]; NSString *authorName = user.displayName; NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); actionText = [[NSString alloc] initWithFormat:formatString, authorName]; diff --git a/Telegram/Watch/Extension/TGNeoSmiliesMessageViewModel.m b/Telegram/Watch/Extension/TGNeoSmiliesMessageViewModel.m index 6fec0fd0c3..35712f46d9 100644 --- a/Telegram/Watch/Extension/TGNeoSmiliesMessageViewModel.m +++ b/Telegram/Watch/Extension/TGNeoSmiliesMessageViewModel.m @@ -28,7 +28,7 @@ const CGFloat TGNeoSmiliesMessageHeight = 39; if (message.cid < 0 && type != TGNeoMessageTypeChannel && !message.outgoing) { - _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int64_t)message.fromUid myUserId:context.userId] attributes:nil]; [self addSubmodel:_authorNameModel]; } diff --git a/Telegram/Watch/Extension/TGNeoStickerMessageViewModel.m b/Telegram/Watch/Extension/TGNeoStickerMessageViewModel.m index 69e878d415..fddb339c5f 100644 --- a/Telegram/Watch/Extension/TGNeoStickerMessageViewModel.m +++ b/Telegram/Watch/Extension/TGNeoStickerMessageViewModel.m @@ -50,7 +50,7 @@ if (message.cid < 0 && !TGPeerIdIsChannel(message.cid) && !message.outgoing) { - _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int64_t)message.fromUid myUserId:context.userId] attributes:nil]; [self addSubmodel:_authorNameModel]; } } diff --git a/Telegram/Watch/Extension/TGNotificationController.h b/Telegram/Watch/Extension/TGNotificationController.h deleted file mode 100644 index 84d102f4de..0000000000 --- a/Telegram/Watch/Extension/TGNotificationController.h +++ /dev/null @@ -1,42 +0,0 @@ -#import -#import - -@interface TGNotificationController : WKUserNotificationInterfaceController - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *forwardHeaderGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardTitleLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardFromLabel; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderImageGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyAuthorNameLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyMessageTextLabel; - -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *messageTextLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *chatTitleLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mediaGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *captionGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *captionLabel; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *wrapperGroup; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mapGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceMap *map; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *durationGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *durationLabel; - -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *titleLabel; -@property (nonatomic, weak) IBOutlet WKInterfaceLabel *subtitleLabel; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *audioGroup; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileIconGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceImage *venueIcon; - -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerWrapperGroup; -@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerGroup; - -@end diff --git a/Telegram/Watch/Extension/TGNotificationController.m b/Telegram/Watch/Extension/TGNotificationController.m deleted file mode 100644 index 55e5fd8ab2..0000000000 --- a/Telegram/Watch/Extension/TGNotificationController.m +++ /dev/null @@ -1,407 +0,0 @@ -#import "TGNotificationController.h" - -#import - -#import "TGWatchCommon.h" -#import "TGStringUtils.h" -#import "TGLocationUtils.h" -#import "WKInterfaceImage+Signals.h" - -#import "TGInputController.h" - -#import "TGMessageViewModel.h" - -#import "TGBridgeMediaSignals.h" -#import "TGBridgeClient.h" -#import "TGBridgeUserCache.h" - -#import -#import - -@interface TGNotificationController() -{ - NSString *_currentAvatarPhoto; - SMetaDisposable *_disposable; -} -@end - -@implementation TGNotificationController - -- (instancetype)init -{ - self = [super init]; - if (self != nil) - { - _disposable = [[SMetaDisposable alloc] init]; - } - return self; -} - -- (void)dealloc -{ - [_disposable dispose]; -} - -- (void)didReceiveNotification:(UNNotification *)notification -{ - UNNotificationContent *content = notification.request.content; - NSString *titleText = content.title; - NSString *bodyText = content.body; - - if (titleText > 0){ - self.nameLabel.hidden = false; - self.nameLabel.text = titleText; - } - self.messageTextLabel.text = bodyText; - - [self processMessageWithUserInfo:content.userInfo defaultTitle:titleText defaultBody:bodyText completion:nil]; -} - -- (void)didReceiveLocalNotification:(UILocalNotification *)localNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler -{ - [self processMessageWithUserInfo:localNotification.userInfo defaultTitle:localNotification.alertTitle defaultBody:localNotification.alertBody completion:completionHandler]; -} - -- (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler -{ - NSString *titleText = nil; - NSString *bodyText = nil; - if ([remoteNotification[@"aps"] respondsToSelector:@selector(objectForKey:)]) { - NSDictionary *aps = remoteNotification[@"aps"]; - if ([aps[@"alert"] respondsToSelector:@selector(objectForKey:)]) { - NSDictionary *alert = aps[@"alert"]; - if ([alert[@"body"] respondsToSelector:@selector(characterAtIndex:)]) { - bodyText = alert[@"body"]; - if ([alert[@"title"] respondsToSelector:@selector(characterAtIndex:)]) { - titleText = alert[@"title"]; - } - } - } else if ([aps[@"alert"] respondsToSelector:@selector(characterAtIndex:)]) { - NSString *alert = aps[@"alert"]; - NSUInteger colonLocation = [alert rangeOfString:@": "].location; - if (colonLocation != NSNotFound) { - titleText = [alert substringToIndex:colonLocation]; - bodyText = [alert substringFromIndex:colonLocation + 2]; - } else { - bodyText = alert; - } - } - } - [self processMessageWithUserInfo:remoteNotification defaultTitle:titleText defaultBody:bodyText completion:completionHandler]; -} - -- (void)processMessageWithUserInfo:(NSDictionary *)userInfo defaultTitle:(NSString *)defaultTitle defaultBody:(NSString *)defaultBody completion:(void (^)(WKUserNotificationInterfaceType))completionHandler -{ - NSString *fromId = userInfo[@"from_id"]; - NSString *chatId = userInfo[@"chat_id"]; - NSString *channelId = userInfo[@"channel_id"]; - NSString *mid = userInfo[@"msg_id"]; - - int64_t peerId = 0; - if (fromId != nil) { - peerId = [fromId longLongValue]; - } else if (chatId != nil) { - peerId = TGPeerIdFromGroupId([chatId integerValue]); - } else if (channelId != nil) { - peerId = TGPeerIdFromChannelId([channelId integerValue]); - } - int32_t messageId = [mid intValue]; - - if (true || peerId == 0 || messageId == 0) - { - if (defaultTitle.length > 0){ - self.nameLabel.hidden = false; - self.nameLabel.text = defaultTitle; - } - self.messageTextLabel.text = defaultBody; - if (completionHandler != nil) - completionHandler(WKUserNotificationInterfaceTypeCustom); - return; - } - - NSLog(@"[Notification] processing message peerId: %lld mid: %d", peerId, messageId); - TGBridgeChatMessageSubscription *subscription = [[TGBridgeChatMessageSubscription alloc] initWithPeerId:peerId messageId:messageId]; - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:subscription]; - - __weak TGNotificationController *weakSelf = self; - SSignal *signal = [[TGBridgeClient instance] sendMessageData:data]; - [_disposable setDisposable:[[signal timeout:4.5 onQueue:[SQueue mainQueue] orSignal:[SSignal single:@0]] startWithNext:^(NSData *messageData) { - __strong TGNotificationController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - if ([messageData isKindOfClass:[NSData class]]) { - NSLog(@"[Notification] Received message data, applying"); - - TGBridgeResponse *response = [NSKeyedUnarchiver unarchiveObjectWithData:messageData]; - NSDictionary *message = response.next; - [strongSelf updateWithMessage:message[TGBridgeMessageKey] users:message[TGBridgeUsersDictionaryKey] chat:message[TGBridgeChatKey] completion:completionHandler]; - } - else { - NSLog(@"[Notification] 4.5 sec timeout, fallback to apns data"); - - strongSelf.nameLabel.hidden = false; - strongSelf.nameLabel.text = defaultTitle; - strongSelf.messageTextLabel.text = defaultBody; - if (completionHandler != nil) - completionHandler(WKUserNotificationInterfaceTypeCustom); - } - } error:^(id error) - { - __strong TGNotificationController *strongSelf = weakSelf; - if (strongSelf == nil) - return; - - NSLog(@"[Notification] getMessage error, fallback to apns data"); - - strongSelf.nameLabel.hidden = false; - strongSelf.nameLabel.text = defaultTitle; - strongSelf.messageTextLabel.text = defaultBody; - if (completionHandler != nil) - completionHandler(WKUserNotificationInterfaceTypeCustom); - } completed:nil]]; -} - -- (void)updateWithMessage:(TGBridgeMessage *)message users:(NSDictionary *)users chat:(TGBridgeChat *)chat completion:(void (^)(WKUserNotificationInterfaceType))completionHandler -{ - [[TGBridgeUserCache instance] storeUsers:[users allValues]]; - - bool mediaGroupHidden = true; - bool mapGroupHidden = true; - bool fileGroupHidden = true; - bool stickerGroupHidden = true; - bool captionGroupHidden = true; - - TGBridgeForwardedMessageMediaAttachment *forwardAttachment = nil; - TGBridgeReplyMessageMediaAttachment *replyAttachment = nil; - NSString *messageText = nil; - - __block NSInteger completionCount = 1; - void (^completionBlock)(void) = ^ - { - completionCount--; - if (completionCount == 0 && completionHandler != nil) - completionHandler(WKUserNotificationInterfaceTypeCustom); - }; - - for (TGBridgeMediaAttachment *attachment in message.media) - { - if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) - { - forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; - } - else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) - { - replyAttachment = (TGBridgeReplyMessageMediaAttachment *)attachment; - } - else if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) - { - mediaGroupHidden = false; - - TGBridgeImageMediaAttachment *imageAttachment = (TGBridgeImageMediaAttachment *)attachment; - - completionCount++; - - CGSize imageSize = CGSizeZero; - [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:nil attachment:imageAttachment message:message notification:true currentPhoto:NULL standalone:true margin:1.5f imageSize:&imageSize isVisible:nil completion:completionBlock]; - - self.mediaGroup.width = imageSize.width; - self.mediaGroup.height = imageSize.height; - - self.durationGroup.hidden = true; - } - else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) - { - mediaGroupHidden = false; - - TGBridgeVideoMediaAttachment *videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; - - completionCount++; - - CGSize imageSize = CGSizeZero; - [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:nil attachment:videoAttachment message:message notification:true currentPhoto:NULL standalone:true margin:1.5f imageSize:&imageSize isVisible:nil completion:completionBlock]; - - self.mediaGroup.width = imageSize.width; - self.mediaGroup.height = imageSize.height; - if (videoAttachment.round) - self.mediaGroup.cornerRadius = imageSize.width / 2.0f; - - self.durationGroup.hidden = false; - - NSInteger durationMinutes = floor(videoAttachment.duration / 60.0); - NSInteger durationSeconds = videoAttachment.duration % 60; - self.durationLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; - } - else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) - { - TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; - - if (documentAttachment.isSticker) - { - stickerGroupHidden = false; - - completionCount++; - - [TGStickerViewModel updateWithMessage:message notification:true isGroup:false context:nil currentDocumentId:NULL authorLabel:nil imageGroup:self.stickerGroup isVisible:nil completion:completionBlock]; - } - else if (documentAttachment.isAudio && documentAttachment.isVoice) - { - fileGroupHidden = false; - - self.titleLabel.text = TGLocalized(@"Message.Audio"); - - NSInteger durationMinutes = floor(documentAttachment.duration / 60.0); - NSInteger durationSeconds = documentAttachment.duration % 60; - self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; - - self.audioGroup.hidden = false; - self.fileIconGroup.hidden = true; - self.venueIcon.hidden = true; - } - else - { - fileGroupHidden = false; - - self.titleLabel.text = documentAttachment.fileName; - self.subtitleLabel.text = [TGStringUtils stringForFileSize:documentAttachment.fileSize precision:2]; - - self.fileIconGroup.hidden = false; - self.audioGroup.hidden = true; - self.venueIcon.hidden = true; - } - } - else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) - { - fileGroupHidden = false; - - TGBridgeAudioMediaAttachment *audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; - - self.titleLabel.text = TGLocalized(@"Message.Audio"); - - NSInteger durationMinutes = floor(audioAttachment.duration / 60.0); - NSInteger durationSeconds = audioAttachment.duration % 60; - self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; - - self.audioGroup.hidden = false; - self.fileIconGroup.hidden = true; - self.venueIcon.hidden = true; - } - else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) - { - mapGroupHidden = false; - - TGBridgeLocationMediaAttachment *locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; - - CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([TGLocationUtils adjustGMapLatitude:locationAttachment.latitude withPixelOffset:-10 zoom:15], locationAttachment.longitude); - self.map.region = MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(0.003, 0.003)); - self.map.centerPinCoordinate = CLLocationCoordinate2DMake(locationAttachment.latitude, locationAttachment.longitude); - - if (locationAttachment.venue != nil) - { - fileGroupHidden = false; - - self.titleLabel.text = locationAttachment.venue.title; - self.subtitleLabel.text = locationAttachment.venue.address; - } - - self.audioGroup.hidden = true; - self.fileIconGroup.hidden = true; - self.venueIcon.hidden = false; - } - else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) - { - fileGroupHidden = false; - - TGBridgeContactMediaAttachment *contactAttachment = (TGBridgeContactMediaAttachment *)attachment; - - self.audioGroup.hidden = true; - self.fileIconGroup.hidden = true; - self.venueIcon.hidden = true; - - self.titleLabel.text = [contactAttachment displayName]; - self.subtitleLabel.text = contactAttachment.prettyPhoneNumber; - } - else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) - { - messageText = [TGMessageViewModel stringForActionAttachment:(TGBridgeActionMediaAttachment *)attachment message:message users:users forChannel:(chat.isChannel && !chat.isChannelGroup)]; - } - else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) - { - fileGroupHidden = false; - - TGBridgeUnsupportedMediaAttachment *unsupportedAttachment = (TGBridgeUnsupportedMediaAttachment *)attachment; - - self.titleLabel.text = unsupportedAttachment.title; - self.subtitleLabel.text = unsupportedAttachment.subtitle; - - self.fileIconGroup.hidden = true; - self.audioGroup.hidden = true; - self.venueIcon.hidden = true; - } - } - - if (messageText == nil) - messageText = message.text; - - id forwardPeer = nil; - if (forwardAttachment != nil) - { - if (TGPeerIdIsChannel(forwardAttachment.peerId)) - forwardPeer = users[@(forwardAttachment.peerId)]; - else - forwardPeer = [[TGBridgeUserCache instance] userWithId:(int32_t)forwardAttachment.peerId]; - } - [TGMessageViewModel updateForwardHeaderGroup:self.forwardHeaderGroup titleLabel:self.forwardTitleLabel fromLabel:self.forwardFromLabel forwardAttachment:forwardAttachment forwardPeer:forwardPeer textColor:[UIColor blackColor]]; - - if (replyAttachment != nil) - { - self.replyHeaderImageGroup.hidden = true; - completionCount++; - } - - [TGMessageViewModel updateReplyHeaderGroup:self.replyHeaderGroup authorLabel:self.replyAuthorNameLabel imageGroup:nil textLabel:self.replyMessageTextLabel titleColor:[UIColor blackColor] subtitleColor:[UIColor hexColor:0x7e7e81] replyAttachment:replyAttachment currentReplyPhoto:NULL isVisible:nil completion:completionBlock]; - - self.mediaGroup.hidden = mediaGroupHidden; - self.mapGroup.hidden = mapGroupHidden; - self.fileGroup.hidden = fileGroupHidden; - self.captionGroup.hidden = captionGroupHidden; - self.stickerGroup.hidden = stickerGroupHidden; - self.stickerWrapperGroup.hidden = stickerGroupHidden; - - self.wrapperGroup.hidden = (self.mediaGroup.hidden && self.mapGroup.hidden && self.fileGroup.hidden && self.stickerGroup.hidden); - - if (chat.isGroup || chat.isChannelGroup) - { - self.chatTitleLabel.text = chat.groupTitle; - self.chatTitleLabel.hidden = false; - } - - self.nameLabel.hidden = false; - if (chat.isChannel && !chat.isChannelGroup) - self.nameLabel.text = chat.groupTitle; - else - self.nameLabel.text = [users[@(message.fromUid)] displayName]; - - self.messageTextLabel.hidden = (messageText.length == 0); - if (!self.messageTextLabel.hidden) - self.messageTextLabel.text = messageText; - - completionBlock(); -} - -- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forNotification:(UNNotification *)notification inputLanguage:(NSString *)inputLanguage -{ - return [TGInputController suggestionsForText:nil]; -} - -- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)localNotification inputLanguage:(NSString *)inputLanguage -{ - return [TGInputController suggestionsForText:nil]; -} - -- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)remoteNotification inputLanguage:(NSString *)inputLanguage -{ - return [TGInputController suggestionsForText:nil]; -} - -@end diff --git a/Telegram/Watch/Extension/TGUserInfoController.h b/Telegram/Watch/Extension/TGUserInfoController.h index 788a4c3e08..2eb0284dff 100644 --- a/Telegram/Watch/Extension/TGUserInfoController.h +++ b/Telegram/Watch/Extension/TGUserInfoController.h @@ -8,14 +8,14 @@ @property (nonatomic, strong) TGBridgeContext *context; @property (nonatomic, readonly) TGBridgeUser *user; -@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, readonly) int64_t userId; @property (nonatomic, readonly) TGBridgeChat *channel; @property (nonatomic, assign) bool disallowCompose; - (instancetype)initWithUser:(TGBridgeUser *)user; -- (instancetype)initWithUserId:(int32_t)userId; +- (instancetype)initWithUserId:(int64_t)userId; - (instancetype)initWithChannel:(TGBridgeChat *)channel; diff --git a/Telegram/Watch/Extension/TGUserInfoController.m b/Telegram/Watch/Extension/TGUserInfoController.m index c3eb9d87e0..5562240334 100644 --- a/Telegram/Watch/Extension/TGUserInfoController.m +++ b/Telegram/Watch/Extension/TGUserInfoController.m @@ -34,7 +34,7 @@ NSString *const TGUserInfoControllerIdentifier = @"TGUserInfoController"; return self; } -- (instancetype)initWithUserId:(int32_t)userId +- (instancetype)initWithUserId:(int64_t)userId { self = [super init]; if (self != nil) @@ -138,7 +138,7 @@ NSString *const TGUserInfoControllerIdentifier = @"TGUserInfoController"; { self.title = TGLocalized(@"Watch.UserInfo.Title"); - int32_t userId = (_context.user != nil) ? (int32_t)_context.user.identifier : _context.userId; + int64_t userId = (_context.user != nil) ? (int64_t)_context.user.identifier : _context.userId; SSignal *remoteUserSignal = [TGBridgeUserInfoSignals userInfoWithUserId:userId]; SSignal *userSignal = nil; diff --git a/Telegram/Watch/Extension/TGWatchColor.h b/Telegram/Watch/Extension/TGWatchColor.h index c16035780f..7a53becefb 100644 --- a/Telegram/Watch/Extension/TGWatchColor.h +++ b/Telegram/Watch/Extension/TGWatchColor.h @@ -9,7 +9,7 @@ @interface TGColor : NSObject -+ (UIColor *)colorForUserId:(int32_t)userId myUserId:(int32_t)myUserId; ++ (UIColor *)colorForUserId:(int64_t)userId myUserId:(int64_t)myUserId; + (UIColor *)colorForGroupId:(int64_t)groupId; + (UIColor *)accentColor; diff --git a/Telegram/Watch/Extension/TGWatchColor.m b/Telegram/Watch/Extension/TGWatchColor.m index afc4a7e960..b53a938689 100644 --- a/Telegram/Watch/Extension/TGWatchColor.m +++ b/Telegram/Watch/Extension/TGWatchColor.m @@ -36,14 +36,14 @@ return colors; } -+ (UIColor *)colorForUserId:(int32_t)userId myUserId:(int32_t)myUserId ++ (UIColor *)colorForUserId:(int64_t)userId myUserId:(int64_t)myUserId { return [self placeholderColors][abs(userId) % 7]; } + (UIColor *)colorForGroupId:(int64_t)groupId { - int32_t peerId = 0; + int64_t peerId = 0; if (TGPeerIdIsGroup(groupId)) { peerId = TGGroupIdFromPeerId(groupId); } else if (TGPeerIdIsChannel(groupId)) { diff --git a/Telegram/Watch/Extension/TGWatchCommon.m b/Telegram/Watch/Extension/TGWatchCommon.m index 34fe8f47f6..5a9e7e1068 100644 --- a/Telegram/Watch/Extension/TGWatchCommon.m +++ b/Telegram/Watch/Extension/TGWatchCommon.m @@ -130,7 +130,7 @@ void TGResetLocalization() TGLocalizedStaticVersion++; } -NSString *TGLocalized(NSString *s) +NSString *TGLocalizedInternal(NSString *s) { static NSString *untranslatedString = nil; @@ -198,3 +198,9 @@ NSString *TGLocalized(NSString *s) return s; } + +// MARK: Swiftgram +NSString *TGLocalized(NSString *s) { + NSString *result = TGLocalizedInternal(s); + return [result stringByReplacingOccurrencesOfString:@"Telegram" withString:@"Swiftgram"]; +} \ No newline at end of file diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeChat.h b/Telegram/Watch/WatchCommonWatch/TGBridgeChat.h index 0e218eb133..9de476e2d6 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeChat.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeChat.h @@ -5,7 +5,7 @@ @property (nonatomic) int64_t identifier; @property (nonatomic) NSTimeInterval date; -@property (nonatomic) int32_t fromUid; +@property (nonatomic) int64_t fromUid; @property (nonatomic, strong) NSString *text; @property (nonatomic, strong) NSArray *media; diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeChat.m b/Telegram/Watch/WatchCommonWatch/TGBridgeChat.m index 973522fa3c..5088233c3e 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeChat.m +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeChat.m @@ -37,7 +37,7 @@ NSString *const TGBridgeChatsArrayKey = @"chats"; { _identifier = [aDecoder decodeInt64ForKey:TGBridgeChatIdentifierKey]; _date = [aDecoder decodeDoubleForKey:TGBridgeChatDateKey]; - _fromUid = [aDecoder decodeInt32ForKey:TGBridgeChatFromUidKey]; + _fromUid = [aDecoder decodeInt64ForKey:TGBridgeChatFromUidKey]; _text = [aDecoder decodeObjectForKey:TGBridgeChatTextKey]; _outgoing = [aDecoder decodeBoolForKey:TGBridgeChatOutgoingKey]; _unread = [aDecoder decodeBoolForKey:TGBridgeChatUnreadKey]; @@ -67,7 +67,7 @@ NSString *const TGBridgeChatsArrayKey = @"chats"; { [aCoder encodeInt64:self.identifier forKey:TGBridgeChatIdentifierKey]; [aCoder encodeDouble:self.date forKey:TGBridgeChatDateKey]; - [aCoder encodeInt32:self.fromUid forKey:TGBridgeChatFromUidKey]; + [aCoder encodeInt64:self.fromUid forKey:TGBridgeChatFromUidKey]; [aCoder encodeObject:self.text forKey:TGBridgeChatTextKey]; [aCoder encodeBool:self.outgoing forKey:TGBridgeChatOutgoingKey]; [aCoder encodeBool:self.unread forKey:TGBridgeChatUnreadKey]; diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeContactMediaAttachment.h b/Telegram/Watch/WatchCommonWatch/TGBridgeContactMediaAttachment.h index a8531272d8..ba69455bb7 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeContactMediaAttachment.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeContactMediaAttachment.h @@ -2,7 +2,7 @@ @interface TGBridgeContactMediaAttachment : TGBridgeMediaAttachment -@property (nonatomic, assign) int32_t uid; +@property (nonatomic, assign) int64_t uid; @property (nonatomic, strong) NSString *firstName; @property (nonatomic, strong) NSString *lastName; @property (nonatomic, strong) NSString *phoneNumber; diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeContext.h b/Telegram/Watch/WatchCommonWatch/TGBridgeContext.h index cdea027feb..ee0cb04788 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeContext.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeContext.h @@ -11,7 +11,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary; - (NSDictionary *)dictionary; -- (TGBridgeContext *)updatedWithAuthorized:(bool)authorized peerId:(int32_t)peerId; +- (TGBridgeContext *)updatedWithAuthorized:(bool)authorized peerId:(int64_t)peerId; - (TGBridgeContext *)updatedWithPreheatData:(NSDictionary *)data; - (TGBridgeContext *)updatedWithMicAccessAllowed:(bool)allowed; diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeContext.m b/Telegram/Watch/WatchCommonWatch/TGBridgeContext.m index df70505a06..e6f5e479cc 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeContext.m +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeContext.m @@ -16,7 +16,7 @@ NSString *const TGBridgeContextStartupDataVersion = @"version"; if (self != nil) { _authorized = [dictionary[TGBridgeContextAuthorized] boolValue]; - _userId = (int32_t)[dictionary[TGBridgeContextUserId] intValue]; + _userId = (int64_t)[dictionary[TGBridgeContextUserId] intValue]; _micAccessAllowed = [dictionary[TGBridgeContextMicAccessAllowed] boolValue]; if (dictionary[TGBridgeContextStartupData] != nil) { diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.h b/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.h index d4bae69554..d9bbb73c87 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.h @@ -53,11 +53,11 @@ typedef NS_ENUM(NSUInteger, TGBridgeMessageDeliveryState) { - (NSArray *)involvedUserIds; - (NSArray *)textCheckingResults; -+ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId; -+ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage; -+ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int32_t)userId; -+ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int32_t)userId; -+ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int32_t)userId localAudioId:(int64_t)localAudioId; ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int64_t)userId; ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int64_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage; ++ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int64_t)userId; ++ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int64_t)userId; ++ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int64_t)userId localAudioId:(int64_t)localAudioId; @end diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.m b/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.m index e66e3313b3..05a3e4e87f 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.m +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeMessage.m @@ -157,12 +157,12 @@ NSString *const TGBridgeMessagesArrayKey = @"messages"; return self.identifier == message.identifier; } -+ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int64_t)userId { return [self temporaryNewMessageForText:text userId:userId replyToMessage:nil]; } -+ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int64_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage { int64_t randomId = 0; arc4random_buf(&randomId, 8); @@ -192,17 +192,17 @@ NSString *const TGBridgeMessagesArrayKey = @"messages"; return message; } -+ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int32_t)userId ++ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int64_t)userId { return [self _temporaryNewMessageForMediaAttachment:sticker userId:userId]; } -+ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int32_t)userId ++ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int64_t)userId { return [self _temporaryNewMessageForMediaAttachment:location userId:userId]; } -+ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int32_t)userId localAudioId:(int64_t)localAudioId ++ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int64_t)userId localAudioId:(int64_t)localAudioId { TGBridgeDocumentMediaAttachment *document = [[TGBridgeDocumentMediaAttachment alloc] init]; document.isAudio = true; @@ -213,7 +213,7 @@ NSString *const TGBridgeMessagesArrayKey = @"messages"; return [self _temporaryNewMessageForMediaAttachment:document userId:userId]; } -+ (instancetype)_temporaryNewMessageForMediaAttachment:(TGBridgeMediaAttachment *)attachment userId:(int32_t)userId ++ (instancetype)_temporaryNewMessageForMediaAttachment:(TGBridgeMediaAttachment *)attachment userId:(int64_t)userId { int64_t randomId = 0; arc4random_buf(&randomId, 8); diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgePeerIdAdapter.h b/Telegram/Watch/WatchCommonWatch/TGBridgePeerIdAdapter.h index c5f0ac92fc..5c646d56dd 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgePeerIdAdapter.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgePeerIdAdapter.h @@ -1,52 +1,120 @@ #ifndef Telegraph_TGPeerIdAdapter_h #define Telegraph_TGPeerIdAdapter_h -static inline bool TGPeerIdIsGroup(int64_t peerId) { - return peerId < 0 && peerId > INT32_MIN; +// Namespace constants based on Swift implementation +#define TG_NAMESPACE_MASK 0x7 +#define TG_NAMESPACE_EMPTY 0x0 +#define TG_NAMESPACE_CLOUD 0x1 +#define TG_NAMESPACE_GROUP 0x2 +#define TG_NAMESPACE_CHANNEL 0x3 +#define TG_NAMESPACE_SECRET_CHAT 0x4 +#define TG_NAMESPACE_ADMIN_LOG 0x5 +#define TG_NAMESPACE_AD 0x6 +#define TG_NAMESPACE_MAX 0x7 + +// Helper functions for bit manipulation +static inline uint32_t TGPeerIdGetNamespace(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + return (uint32_t)((data >> 32) & TG_NAMESPACE_MASK); +} + +static inline int64_t TGPeerIdGetId(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + uint64_t idHighBits = (data >> (32 + 3)) << 32; + uint64_t idLowBits = data & 0xffffffff; + return (int64_t)(idHighBits | idLowBits); +} + +static inline int64_t TGPeerIdMake(uint32_t namespaceId, int64_t id) { + uint64_t data = 0; + uint64_t idBits = (uint64_t)id; + uint64_t idLowBits = idBits & 0xffffffff; + uint64_t idHighBits = (idBits >> 32) & 0xffffffff; + + data |= ((uint64_t)(namespaceId & TG_NAMESPACE_MASK)) << 32; + data |= (idHighBits << (32 + 3)); + data |= idLowBits; + + return (int64_t)data; +} + +// Updated peer type checks +static inline bool TGPeerIdIsEmpty(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_EMPTY; } static inline bool TGPeerIdIsUser(int64_t peerId) { - return peerId > 0 && peerId < INT32_MAX; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CLOUD; +} + +static inline bool TGPeerIdIsGroup(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_GROUP; } static inline bool TGPeerIdIsChannel(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 2 && peerId > ((int64_t)INT32_MIN) * 3; -} - -static inline bool TGPeerIdIsAdminLog(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 3 && peerId > ((int64_t)INT32_MIN) * 4; -} - -static inline int32_t TGChannelIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsChannel(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 2 - peerId); - } else { - return 0; - } -} - -static inline int64_t TGPeerIdFromChannelId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 2 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromAdminLogId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 3 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromGroupId(int32_t groupId) { - return -groupId; -} - -static inline int32_t TGGroupIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsGroup(peerId)) { - return (int32_t)-peerId; - } else { - return 0; - } + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CHANNEL; } static inline bool TGPeerIdIsSecretChat(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) && peerId > ((int64_t)INT32_MIN) * 2; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_SECRET_CHAT; +} + +static inline bool TGPeerIdIsAdminLog(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_ADMIN_LOG; +} + +static inline bool TGPeerIdIsAd(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_AD; +} + +// Conversion functions +static inline int64_t TGPeerIdFromUserId(int64_t userId) { + return TGPeerIdMake(TG_NAMESPACE_CLOUD, userId); +} + +static inline int64_t TGPeerIdFromGroupId(int64_t groupId) { + return TGPeerIdMake(TG_NAMESPACE_GROUP, groupId); +} + +static inline int64_t TGPeerIdFromChannelId(int64_t channelId) { + return TGPeerIdMake(TG_NAMESPACE_CHANNEL, channelId); +} + +static inline int64_t TGPeerIdFromSecretChatId(int64_t secretChatId) { + return TGPeerIdMake(TG_NAMESPACE_SECRET_CHAT, secretChatId); +} + +static inline int64_t TGPeerIdFromAdminLogId(int64_t adminLogId) { + return TGPeerIdMake(TG_NAMESPACE_ADMIN_LOG, adminLogId); +} + +static inline int64_t TGPeerIdFromAdId(int64_t adId) { + return TGPeerIdMake(TG_NAMESPACE_AD, adId); +} + +// Extract IDs +static inline int64_t TGUserIdFromPeerId(int64_t peerId) { + return TGPeerIdIsUser(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGGroupIdFromPeerId(int64_t peerId) { + return TGPeerIdIsGroup(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGChannelIdFromPeerId(int64_t peerId) { + return TGPeerIdIsChannel(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGSecretChatIdFromPeerId(int64_t peerId) { + return TGPeerIdIsSecretChat(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdminLogIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAdminLog(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAd(peerId) ? TGPeerIdGetId(peerId) : 0; } #endif diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeUser.h b/Telegram/Watch/WatchCommonWatch/TGBridgeUser.h index 9aca3fa520..a8ab7839be 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeUser.h +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeUser.h @@ -49,10 +49,10 @@ typedef NS_ENUM(NSUInteger, TGBridgeBotKind) { @interface TGBridgeUserChange : NSObject -@property (nonatomic, readonly) int32_t userIdentifier; +@property (nonatomic, readonly) int64_t userIdentifier; @property (nonatomic, readonly) NSDictionary *fields; -- (instancetype)initWithUserIdentifier:(int32_t)userIdentifier fields:(NSDictionary *)fields; +- (instancetype)initWithUserIdentifier:(int64_t)userIdentifier fields:(NSDictionary *)fields; @end diff --git a/Telegram/Watch/WatchCommonWatch/TGBridgeUser.m b/Telegram/Watch/WatchCommonWatch/TGBridgeUser.m index 4c0fed8d97..ba6fd60388 100644 --- a/Telegram/Watch/WatchCommonWatch/TGBridgeUser.m +++ b/Telegram/Watch/WatchCommonWatch/TGBridgeUser.m @@ -255,7 +255,7 @@ NSString *const TGBridgeUserChangeFieldsKey = @"fields"; @implementation TGBridgeUserChange -- (instancetype)initWithUserIdentifier:(int32_t)userIdentifier fields:(NSDictionary *)fields +- (instancetype)initWithUserIdentifier:(int64_t)userIdentifier fields:(NSDictionary *)fields { self = [super init]; if (self != nil) @@ -271,7 +271,7 @@ NSString *const TGBridgeUserChangeFieldsKey = @"fields"; self = [super init]; if (self != nil) { - _userIdentifier = [aDecoder decodeInt32ForKey:TGBridgeUserChangeIdentifierKey]; + _userIdentifier = [aDecoder decodeInt64ForKey:TGBridgeUserChangeIdentifierKey]; _fields = [aDecoder decodeObjectForKey:TGBridgeUserChangeFieldsKey]; } return self; @@ -279,7 +279,7 @@ NSString *const TGBridgeUserChangeFieldsKey = @"fields"; - (void)encodeWithCoder:(NSCoder *)aCoder { - [aCoder encodeInt32:self.userIdentifier forKey:TGBridgeUserChangeIdentifierKey]; + [aCoder encodeInt64:self.userIdentifier forKey:TGBridgeUserChangeIdentifierKey]; [aCoder encodeObject:self.fields forKey:TGBridgeUserChangeFieldsKey]; } diff --git a/WORKSPACE b/WORKSPACE index 44c89b9780..1d1f929b0f 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", @@ -106,6 +107,14 @@ provisioning_profile_repository( name = "local_provisioning_profiles", ) +# MARK: Swiftgram +new_git_repository( + name = "flex_sdk", + remote = "https://github.com/FLEXTool/FLEX.git", + commit = "2bfba6715eff664ef84a02e8eb0ad9b5a609c684", + build_file = "@//Swiftgram/FLEX:FLEX.BUILD" +) + local_repository( name = "build_configuration", path = "build-input/configuration-repository", diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index 835ecff11c..877f3533dd 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -9,6 +9,7 @@ from BuildEnvironment import run_executable_with_output, check_run_system class BuildConfiguration: def __init__(self, + sg_config, bundle_id, api_id, api_hash, @@ -22,6 +23,7 @@ class BuildConfiguration: 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 @@ class BuildConfiguration: 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 67cbb89c3e..b285f88262 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -274,7 +274,7 @@ class BazelCommandLine: 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'] @@ -660,24 +660,24 @@ 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') + dsym_paths = glob.glob('bazel-bin/Telegram/*.dSYM') + glob.glob('bazel-out/watchos_arm64_32-opt-watchos-arm64_32-min9.0-applebin_watchos-ST-*/bin/Telegram/TelegramWatchApp_dsyms/*.dSYM') for dsym_path in dsym_paths: file_name = os.path.basename(dsym_path) shutil.copytree(dsym_path, artifacts_path + '/DSYMs/{}'.format(file_name)) @@ -685,7 +685,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 17d436a33d..212e99ca8d 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -34,6 +34,9 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, project_bazel_arguments.append(argument) project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)] + if target_name == "Swiftgram/Playground": + project_bazel_arguments += ["--swiftcopt=-no-warnings-as-errors", "--copt=-Wno-error"]#, "--swiftcopt=-DSWIFTGRAM_PLAYGROUND", "--copt=-DSWIFTGRAM_PLAYGROUND=1"] + if target_name == 'Telegram': if disable_extensions: project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)] @@ -49,7 +52,7 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, file.write('build ' + argument + '\n') call_executable(bazel_generate_arguments) - + xcodeproj_path = '{}.xcodeproj'.format(app_target_spec.replace(':', '/')) return xcodeproj_path diff --git a/build-system/bazel-rules/rules_xcodeproj b/build-system/bazel-rules/rules_xcodeproj index 41929acc4c..44b6f046d9 160000 --- a/build-system/bazel-rules/rules_xcodeproj +++ b/build-system/bazel-rules/rules_xcodeproj @@ -1 +1 @@ -Subproject commit 41929acc4c7c1da973c77871d0375207b9d0806f +Subproject commit 44b6f046d95b84933c1149fbf7f9d81fd4e32020 diff --git a/build-system/template_minimal_development_configuration.json b/build-system/template_minimal_development_configuration.json index 1aad0aed95..7c885537ac 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 0000000000..0ca4fe438e --- /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 fb2ccfc448..fb83084f8a 100644 --- a/submodules/AccountContext/BUILD +++ b/submodules/AccountContext/BUILD @@ -1,5 +1,10 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGIAP:SGIAP" +] + swift_library( name = "AccountContext", module_name = "AccountContext", @@ -9,7 +14,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 6cad8ed0c5..495e9be9a4 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1,3 +1,5 @@ +import SGSimpleSettings +import SGIAP import Foundation import UIKit import AsyncDisplayKit @@ -808,6 +810,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, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) @@ -1021,6 +1025,13 @@ public protocol SharedAccountContext: AnyObject { var automaticMediaDownloadSettings: Signal { get } var currentAutodownloadSettings: Atomic { get } var immediateExperimentalUISettings: ExperimentalUISettings { get } + // MARK: Swiftgram + var immediateSGStatus: SGStatus { get } + var SGIAP: SGIAPManager? { get } + func makeSGProController(context: AccountContext) -> ViewController + func makeSGPayWallController(context: AccountContext) -> ViewController? + func makeSGUpdateIOSController() -> ViewController + var currentInAppNotificationSettings: Atomic { get } var currentMediaInputSettings: Atomic { get } var currentStickerSettings: Atomic { get } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 5931ae17b1..1561f880d6 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 @@ -65,6 +67,8 @@ public final class ChatMessageItemAssociatedData: Equatable { public let showSensitiveContent: Bool public init( + translateToLanguageSG: String? = nil, + translationSettings: TranslationSettings? = nil, automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, @@ -98,6 +102,8 @@ public final class ChatMessageItemAssociatedData: Equatable { isInline: Bool = false, showSensitiveContent: Bool = false ) { + self.translateToLanguageSG = translateToLanguageSG + self.translationSettings = translationSettings self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId self.automaticDownloadNetworkType = automaticDownloadNetworkType @@ -136,6 +142,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 } @@ -965,6 +977,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() @@ -1017,6 +1030,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 1966a168ea..aec0fa5167 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/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index aef3c6d9ff..144bc40508 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -47,6 +47,7 @@ public enum ChatListDisabledPeerReason { public final class PeerSelectionControllerParams { public let context: AccountContext + public let forceHideNames: Bool public let updatedPresentationData: (initial: PresentationData, signal: Signal)? public let filter: ChatListNodePeersFilter public let requestPeerType: [ReplyMarkupButtonRequestPeerType]? @@ -69,6 +70,7 @@ public final class PeerSelectionControllerParams { public init( context: AccountContext, + forceHideNames: Bool = false, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil, @@ -90,6 +92,7 @@ public final class PeerSelectionControllerParams { immediatelyActivateMultipleSelection: Bool = false ) { self.context = context + self.forceHideNames = forceHideNames self.updatedPresentationData = updatedPresentationData self.filter = filter self.requestPeerType = requestPeerType diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index e1b5e521bb..2fe0ed0d67 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -273,9 +273,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, + // MARK: Swiftgram + showPremiumGiftInAttachMenu: false, // data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, + showPremiumGiftInTextField: false, // data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField + giveawayGiftsPurchaseAvailable: false, // data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, diff --git a/submodules/AccountUtils/Sources/AccountUtils.swift b/submodules/AccountUtils/Sources/AccountUtils.swift index 44d09f560f..df544dc075 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 = 500 +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 @@ -15,7 +18,7 @@ public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool func accountWithPeer(_ context: AccountContext) -> Signal<(AccountContext, EnginePeer, Int32)?, NoError> { return combineLatest(context.account.postbox.peerView(id: context.account.peerId), renderedTotalUnreadCount(accountManager: sharedContext.accountManager, engine: context.engine)) |> map { view, totalUnreadCount -> (EnginePeer?, Int32) in - return (view.peers[view.peerId].flatMap(EnginePeer.init), totalUnreadCount.0) + return (view.peers[view.peerId].flatMap(EnginePeer.init) ?? EnginePeer.init(TelegramUser(id: view.peerId, accessHash: nil, firstName: "RESTORED", lastName: "\(view.peerId.id._internalGetInt64Value())", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags(), emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)), totalUnreadCount.0) } |> distinctUntilChanged { lhs, rhs in if lhs.0 != rhs.0 { @@ -49,3 +52,18 @@ public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool } } } + +// MARK: Swiftgram +public func getContextForUserId(context: AccountContext, userId: Int64) -> Signal { + if context.account.peerId.id._internalGetInt64Value() == userId { + return .single(context) + } + return context.sharedContext.activeAccountContexts + |> take(1) + |> map { _, activeAccounts, _ -> AccountContext? in + if let account = activeAccounts.first(where: { $0.1.account.peerId.id._internalGetInt64Value() == userId }) { + return account.1 + } + return nil + } +} diff --git a/submodules/AppLock/Sources/AppLock.swift b/submodules/AppLock/Sources/AppLock.swift index b30194cc3a..37bd9b62b8 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/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index 8539e94668..ef60f54cab 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -1,5 +1,10 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgDeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGInputToolbar:SGInputToolbar" +] + swift_library( name = "AttachmentTextInputPanelNode", module_name = "AttachmentTextInputPanelNode", @@ -9,7 +14,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgDeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 261ccc1b6d..86a36b403d 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -1,3 +1,8 @@ +// MARK: Swiftgram +import SGInputToolbar +import SwiftUI +import SGSimpleSettings + import Foundation import UIKit import Display @@ -287,6 +292,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private let hapticFeedback = HapticFeedback() + // MARK: Swiftgram + // private var toolbarHostingController: UIViewController? //Any? // UIHostingController? + private var toolbarNode: ASDisplayNode? + public var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -498,6 +507,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self?.maxCaptionLength = maxCaptionLength }) } + + // MARK: Swiftgram + self.initToolbarIfNeeded(context: context) } public var sendPressed: ((NSAttributedString?) -> Void)? @@ -624,6 +636,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.view.addGestureRecognizer(recognizer) textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + self.initToolbarIfNeeded(context: self.context) } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { @@ -929,7 +942,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.actionButtons.updateAccessibility() - return panelHeight + // MARK: Swiftgram + var toolbarOffset: CGFloat = 0.0 + toolbarOffset = layoutToolbar(transition: transition, panelHeight: panelHeight, width: width, leftInset: leftInset, rightInset: rightInset) + + return panelHeight + toolbarOffset } private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { @@ -1909,3 +1926,99 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return nil } } + +// MARK: Swiftgram +extension AttachmentTextInputPanelNode { + + func initToolbarIfNeeded(context: AccountContext) { + guard #available(iOS 13.0, *) else { return } + guard SGSimpleSettings.shared.inputToolbar else { return } + guard context.sharedContext.immediateSGStatus.status > 1 else { return } + guard self.toolbarNode == nil else { return } + let toolbarView = ChatToolbarView( + onQuote: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesQuote(strongSelf) + }, + onSpoiler: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesSpoiler(strongSelf) + }, + onBold: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesBold(strongSelf) + }, + onItalic: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesItalic(strongSelf) + }, + onMonospace: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesMonospace(strongSelf) + }, + onLink: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesLink(self!) + }, + onStrikethrough: { [weak self] + in guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesStrikethrough(strongSelf) + }, + onUnderline: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesUnderline(strongSelf) + }, + onCode: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesCodeBlock(strongSelf) + }, + onNewLine: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSetNewLine() + }, + // TODO(swiftgram): Binding + showNewLine: .constant(true), //.constant(self.sendWithReturnKey) + onClearFormatting: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(forceRemoveAll: true, current, attribute: ChatTextInputAttributes.allAttributes[0], value: nil), inputMode) + } + } + ) + let toolbarHostingController = UIHostingController(rootView: toolbarView) + toolbarHostingController.view.backgroundColor = .clear + let toolbarNode = ASDisplayNode { toolbarHostingController.view } + self.toolbarNode = toolbarNode + // assigning toolbarHostingController bugs responsivness and overrides layout + // self.toolbarHostingController = toolbarHostingController + + // Disable "Swipe to go back" gesture when touching scrollview + self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in + if let self, let _ = self.toolbarNode?.view.hitTest(point, with: nil) { + return false + } + return true + } + self.addSubnode(toolbarNode) + } + + func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) -> CGFloat { + var toolbarHeight: CGFloat = 0.0 + var toolbarSpacing: CGFloat = 0.0 + if let toolbarNode = self.toolbarNode { + toolbarHeight = 44.0 + toolbarSpacing = 1.0 + transition.updateFrame(node: toolbarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight))) + } + return toolbarHeight + toolbarSpacing + } +} diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index f99be460cd..83dd9c5861 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -948,9 +948,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 a0657ad3e9..8346564942 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 0ddd8a45c6..1c5590cc57 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 @@ -596,11 +598,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 74fd3996ef..459be125db 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 e32f74f78a..9dcf81e51a 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 7ac35f1be8..aef09c3185 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 9ff7cf8e7b..1e09867074 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 a4f25b28d4..6f51a025a5 100644 --- a/submodules/BuildConfig/Sources/BuildConfig.m +++ b/submodules/BuildConfig/Sources/BuildConfig.m @@ -70,6 +70,7 @@ API_AVAILABLE(ios(10)) @interface BuildConfig () { NSData * _Nullable _bundleData; + NSString * _Nonnull _sgConfig; int32_t _apiId; NSString * _Nonnull _apiHash; NSString * _Nullable _appCenterId; @@ -127,6 +128,7 @@ API_AVAILABLE(ios(10)) - (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 cc5499507a..73567ab89d 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 df644f0da2..fbfc45343c 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 @@ -367,6 +369,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 b1491734a3..88363c2793 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 e5cebaf1af..b573287b55 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -357,7 +357,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 c28d06df04..1e26beb2be 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 052e707e58..989856e4d3 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 @@ -379,12 +382,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: @@ -742,8 +756,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() } @@ -936,6 +973,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 { @@ -1626,12 +1665,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 @@ -1650,6 +1693,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 { @@ -2005,6 +2050,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()]) @@ -2107,11 +2154,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] = [] @@ -3417,6 +3476,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) @@ -3461,6 +3540,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() { @@ -3487,6 +3569,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 @@ -3505,7 +3590,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 } @@ -3825,12 +3919,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 } @@ -3868,13 +3973,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 @@ -3899,7 +4010,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 { @@ -3948,6 +4059,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)) } } @@ -4651,7 +4765,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 { @@ -6443,11 +6557,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) } @@ -6494,6 +6612,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 @@ -6562,6 +6688,7 @@ private final class ChatListLocationContext { case .chatList: if !hideNetworkActivityStatus { self.titleDisposable = combineLatest(queue: .mainQueue(), + hideStoriesSignal, networkState, hasProxy, passcode, @@ -6570,12 +6697,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, @@ -6784,7 +6912,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 { @@ -6802,6 +6932,7 @@ private final class ChatListLocationContext { } private func updateChatList( + hideStories: Bool, networkState: AccountNetworkState, proxy: (Bool, Bool), passcode: (Bool, Bool), @@ -6918,7 +7049,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 @@ -6931,6 +7062,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( @@ -7164,3 +7318,15 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) } } + +// 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 357cf6e848..e8b81b4a48 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 @@ -492,8 +493,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 { @@ -515,7 +516,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 @@ -544,8 +545,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: @@ -578,7 +584,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 @@ -590,8 +596,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 @@ -638,7 +644,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? @@ -675,7 +681,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 @@ -1051,6 +1057,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 { @@ -1132,6 +1142,10 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { openArchiveSettings?() }) + // MARK: Swiftgram + self.inlineTabContainerNode = ChatListFilterTabContainerNode(inline: true, context: context) + self.appleStyleTabContainerNode = AppleStyleFoldersNode(context: context) + self.controller = controller super.init() @@ -1144,6 +1158,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) } @@ -1600,6 +1618,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 { @@ -1633,6 +1655,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 4d647eb616..046cb88873 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -6,6 +6,7 @@ import TelegramCore import TelegramPresentationData import TextNodeWithEntities import AccountContext +import SGSimpleSettings private final class ItemNodeDeleteButtonNode: HighlightableButtonNode { private let pressed: () -> Void @@ -331,6 +332,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) @@ -377,6 +383,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) @@ -574,7 +585,11 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } } - public init(context: AccountContext) { + // MARK: Swiftgram + public let inline: Bool + private var backgroundNode: NavigationBackgroundNode? = nil + + public init(inline: Bool = false, context: AccountContext) { self.context = context self.scrollNode = ASScrollNode() @@ -582,6 +597,13 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.selectedLineNode.displaysAsynchronously = false self.selectedLineNode.displayWithoutProcessing = true + // MARK: Swiftgram + self.inline = inline + if self.inline { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.backgroundNode = NavigationBackgroundNode(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor) + } + super.init() self.scrollNode.view.showsHorizontalScrollIndicator = false @@ -592,7 +614,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) @@ -740,13 +764,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) } @@ -775,6 +811,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 { @@ -860,7 +901,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 { @@ -907,7 +948,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 @@ -930,7 +971,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) @@ -984,7 +1025,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 191118b6e6..17916df716 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -4087,7 +4087,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 } @@ -4127,6 +4134,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 7205b7d450..c2284a67e0 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 @@ -656,7 +657,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 { @@ -744,7 +745,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 { @@ -1666,7 +1667,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 @@ -1734,8 +1736,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 @@ -1956,6 +1958,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)) @@ -2203,9 +2209,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 { @@ -2273,7 +2279,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { contentData = .group(peers: groupPeers) hideAuthor = true } - + // MARK: Swiftgram + if sgCompactChatList { hideAuthor = true }; var attributedText: NSAttributedString var hasDraft = false @@ -2301,7 +2308,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 { @@ -3329,12 +3337,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 } @@ -3459,6 +3470,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 = [] } @@ -3508,7 +3520,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) @@ -3663,7 +3676,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 @@ -3998,16 +4012,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent) + // MARK: Swiftgram + let sizeFactor = item.presentationData.fontSize.itemListBaseFontSize / 17.0 + var nextBadgeX: CGFloat = contentRect.maxX if let _ = currentBadgeBackgroundImage { - let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - 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) nextBadgeX -= badgeLayout.width + 6.0 } - if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil { - let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, 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) nextBadgeX -= mentionBadgeLayout.width + 6.0 @@ -4018,7 +4034,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.pinnedIconNode.isHidden = false let pinnedIconSize = currentPinnedIconImage.size - let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height) + let pinnedIconFrame = CGRect(x: nextBadgeX - 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 nextBadgeX -= pinnedIconSize.width + 6.0 @@ -4032,7 +4048,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let actionButtonTopInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 5.0 / 17.0) let actionButtonBottomInset = floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0) - let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset) + var actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset) + // MARK: Swiftgram + actionButtonSize.width = actionButtonSize.width / (sgCompactChatList ? 1.5 : 1.0) + actionButtonSize.height = actionButtonSize.height / (sgCompactChatList ? 1.5 : 1.0) + // var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height) actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0)) @@ -4070,7 +4090,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { actionButtonNode.frame = actionButtonFrame actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size) - actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size) + actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset / (sgCompactChatList ? 1.5 : 1.0)), size: actionButtonTitleNodeLayout.size) nextBadgeX -= actionButtonSize.width + 6.0 } else { @@ -4750,7 +4770,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 5f679acdc0..9b1717c972 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?, Bool) -> Void let disabledPeerSelected: (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void let togglePeerSelected: (EnginePeer, Int64?) -> Void @@ -134,6 +137,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?, Bool) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, togglePeerSelected: @escaping (EnginePeer, Int64?) -> Void, @@ -180,6 +185,7 @@ public final class ChatListNodeInteraction { openUrl: @escaping (String) -> Void ) { self.activateSearch = activateSearch + self.openSGAnnouncement = openSGAnnouncement self.peerSelected = peerSelected self.disabledPeerSelected = disabledPeerSelected self.togglePeerSelected = togglePeerSelected @@ -756,6 +762,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: @@ -769,6 +777,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case .setupBirthday: nodeInteraction?.openBirthdaySetup() case let .birthdayPremiumGift(peers, birthdays): + // TODO(swiftgram): Open user's profile instead of gift nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break @@ -1106,6 +1115,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: @@ -1380,6 +1391,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) @@ -1387,8 +1401,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)) @@ -1429,6 +1444,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) @@ -2021,6 +2061,7 @@ public final class ChatListNode: ListView { }) let suggestedChatListNoticeSignal: Signal = combineLatest( + getSGProvidedSuggestions(account: context.account), context.engine.notices.getServerProvidedSuggestions(), context.engine.notices.getServerDismissedSuggestions(), twoStepData, @@ -2033,9 +2074,16 @@ public final class ChatListNode: ListView { starsSubscriptionsContextPromise.get(), accountFreezeConfiguration ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in + |> mapToSignal { sgSuggestionsData, suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in let (accountPeer, birthday) = data + // 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)) } @@ -2093,8 +2141,12 @@ public final class ChatListNode: ListView { } else if suggestions.contains(.gracePremium) { return .single(.premiumGrace) } 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 726792f7ab..e780a26240 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 d7373fa66d..6f41a23198 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -189,6 +189,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 aa89868121..c3fde0a9ea 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -63,6 +63,8 @@ public enum ChatTranslationDisplayType { public final class ChatPanelInterfaceInteraction { public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void + public let sgSelectLastWordIfIdle: () -> Void + public let sgSetNewLine: () -> Void public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let cancelMessageSelection: (ContainedViewLayoutTransition) -> Void @@ -71,9 +73,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 @@ -189,9 +191,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, @@ -414,6 +416,93 @@ public final class ChatPanelInterfaceInteraction { self.chatController = chatController self.statuses = statuses + + // MARK: Swiftgram + self.sgSelectLastWordIfIdle = { + updateTextInputStateAndMode { current, inputMode in + // No changes to current selection + if !current.selectionRange.isEmpty { + return (current, inputMode) + } + + let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString() + + // If text is empty or cursor is at the start, return current state + guard inputText.length > 0, current.selectionRange.lowerBound > 0 else { + return (current, inputMode) + } + + let plainText = inputText.string + let nsString = plainText as NSString + + // Create character set for word boundaries + let wordBoundaries = CharacterSet.whitespacesAndNewlines + + // Start from cursor position instead of end of text + var endIndex = current.selectionRange.lowerBound - 1 + + // Find last non-whitespace character before cursor + while endIndex >= 0 && + (nsString.substring(with: NSRange(location: endIndex, length: 1)) as NSString) + .rangeOfCharacter(from: wordBoundaries).location != NSNotFound { + endIndex -= 1 + } + + // If we only had whitespace before cursor, return current state + guard endIndex >= 0 else { + return (current, inputMode) + } + + // Find start of the current word by looking backwards for whitespace + var startIndex = endIndex + while startIndex > 0 { + let char = nsString.substring(with: NSRange(location: startIndex - 1, length: 1)) + if (char as NSString).rangeOfCharacter(from: wordBoundaries).location != NSNotFound { + break + } + startIndex -= 1 + } + + // Create range for the word at cursor + let wordLength = endIndex - startIndex + 1 + let wordRange = NSRange(location: startIndex, length: wordLength) + + // Create new selection range + let newSelectionRange = wordRange.location ..< (wordRange.location + wordLength) + + return (ChatTextInputState(inputText: inputText, selectionRange: newSelectionRange), inputMode) + } + } + self.sgSetNewLine = { + updateTextInputStateAndMode { current, inputMode in + let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString() + + // Check if there's selected text + let hasSelection = current.selectionRange.count > 0 + + if hasSelection { + // Move selected text to new line + let selectedText = inputText.attributedSubstring(from: NSRange(current.selectionRange)) + let newLineAttr = NSAttributedString(string: "\n") + + // Insert newline and selected text + inputText.replaceCharacters(in: NSRange(current.selectionRange), with: newLineAttr) + inputText.insert(selectedText, at: current.selectionRange.lowerBound + 1) + + // Update selection range to end of moved text + let newPosition = current.selectionRange.lowerBound + 1 + selectedText.length + return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode) + } else { + // Simple newline insertion at current position + let attributedString = NSAttributedString(string: "\n") + inputText.replaceCharacters(in: NSRange(current.selectionRange), with: attributedString) + + // Update cursor position + let newPosition = current.selectionRange.lowerBound + attributedString.length + return (ChatTextInputState(inputText: inputText, selectionRange: newPosition ..< newPosition), inputMode) + } + } + } } public convenience init( @@ -431,9 +520,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/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift index a0f301fccf..0c1ff4393b 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatTextFormat.swift @@ -3,15 +3,15 @@ import TextFormat import TelegramCore import AccountContext -public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, attribute: NSAttributedString.Key, value: Any?) -> ChatTextInputState { +public func chatTextInputAddFormattingAttribute(forceRemoveAll: Bool = false, _ state: ChatTextInputState, attribute: NSAttributedString.Key, value: Any?) -> ChatTextInputState { if !state.selectionRange.isEmpty { let nsRange = NSRange(location: state.selectionRange.lowerBound, length: state.selectionRange.count) var addAttribute = true var attributesToRemove: [NSAttributedString.Key] = [] state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in for (key, _) in attributes { - if key == attribute { - if nsRange == range { + if key == attribute || forceRemoveAll { + if nsRange == range || forceRemoveAll { addAttribute = false attributesToRemove.append(key) } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index c75dc8abf3..c4df147173 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -69,6 +69,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, @@ -90,6 +91,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 8b13789179..0000000000 --- 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 e5140c9846..aa2cfad65e 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 @@ -639,6 +642,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() } @@ -1422,6 +1497,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)?, @@ -1447,6 +1523,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 186e7ded8c..2acf3fbd5f 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 2e519ff10c..760f0fb3fb 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 553c5023c8..439190310e 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 143f6d3439..ae6b91a97b 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 @@ -45,6 +50,7 @@ private final class DebugControllerArguments { } private enum DebugControllerSection: Int32 { + case swiftgram case sticker case logs case logging @@ -57,6 +63,8 @@ private enum DebugControllerSection: Int32 { } private enum DebugControllerEntry: ItemListNodeEntry { + case SGDebug(PresentationTheme) + case sendSGLogs(PresentationTheme) case testStickerImport(PresentationTheme) case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) @@ -121,6 +129,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: @@ -148,6 +158,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: @@ -280,6 +295,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 { @@ -379,9 +401,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) @@ -428,7 +461,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() @@ -443,7 +476,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) @@ -1469,9 +1502,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)) @@ -1491,7 +1528,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 dad4b4778d..e198b6b727 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -385,6 +385,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 96c8ec0eb8..2aed399790 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 b716d17fb4..ed893d236e 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/BUILD b/submodules/GalleryUI/BUILD index 1ebf1cad4c..ba0887814c 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -1,5 +1,9 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings" +] + swift_library( name = "GalleryUI", module_name = "GalleryUI", @@ -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/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index e161b6d5b5..3fb6723a3c 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -529,6 +529,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 19eadbaba7..e79d1c6913 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 9d5fe45f50..e296378a06 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -715,6 +715,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 f4bf3b8f89..c4fe76a3c4 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -2681,6 +2682,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func maybePerformActionForSwipeDismiss() -> Bool { + if #available(iOS 15.0, *) { + if SGSimpleSettings.shared.videoPIPSwipeDirection != SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue { + return false + } + } + if let data = self.context.currentAppConfiguration.with({ $0 }).data { if let _ = data["ios_killswitch_disable_swipe_pip"] { return false @@ -3559,16 +3566,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/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index fe3759663f..da421c79ab 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -240,12 +240,12 @@ public final class InAppPurchaseManager: NSObject { super.init() - SKPaymentQueue.default().add(self) + // SKPaymentQueue.default().add(self) // MARK: Swiftgram self.requestProducts() } deinit { - SKPaymentQueue.default().remove(self) + // SKPaymentQueue.default().remove(self) // MARK: Swiftgram } var canMakePayments: Bool { @@ -253,6 +253,7 @@ public final class InAppPurchaseManager: NSObject { } private func requestProducts() { + if ({ return true }()) { return } // MARK: Swiftgram Logger.shared.log("InAppPurchaseManager", "Requesting products") let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers)) productRequest.delegate = self @@ -310,7 +311,7 @@ public final class InAppPurchaseManager: NSObject { let payment = SKMutablePayment(product: product.skProduct) payment.applicationUsername = accountPeerId payment.quantity = Int(quantity) - SKPaymentQueue.default().add(payment) + // SKPaymentQueue.default().add(payment) // MARK: Swiftgram let productIdentifier = payment.productIdentifier let signal = Signal { subscriber in diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index fe473f69e4..5fd3e8b5d7 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -62,6 +62,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone let label: String let attributedLabel: NSAttributedString? let labelStyle: ItemListDisclosureLabelStyle + let centerLabelAlignment: Bool let additionalDetailLabel: String? let additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor public let sectionId: ItemListSectionId @@ -73,7 +74,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone 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, titleBadge: String? = 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, titleBadge: String? = 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 @@ -88,6 +89,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone self.labelStyle = labelStyle self.label = label self.attributedLabel = attributedLabel + self.centerLabelAlignment = centerLabelAlignment self.additionalDetailLabel = additionalDetailLabel self.additionalDetailLabelColor = additionalDetailLabelColor self.sectionId = sectionId @@ -685,7 +687,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 @@ -693,7 +695,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 81dc1224b2..3c3a8c48c1 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 12a0296d78..b8e8dca3ce 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 ba44c28837..2009462901 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 075896120d..59af6c33e5 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 hasCoverButton:(bool)hasCoverButton; +- (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 hasCoverButton:(bool)hasCoverButton; - (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 e276b9c85a..734b563475 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 hasCoverButton:(bool)hasCoverButton; +- (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 hasCoverButton:(bool)hasCoverButton; - (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 c696c3a423..69181c51ff 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/TGMediaVideoConverter.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h index e110fed1a7..3686961190 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h @@ -2,6 +2,11 @@ #import +// MARK: Swiftgram +#import +#import +// + @interface TGMediaVideoFileWatcher : NSObject { NSURL *_fileURL; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPeerIdAdapter.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPeerIdAdapter.h index 800041248e..5c646d56dd 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPeerIdAdapter.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPeerIdAdapter.h @@ -1,76 +1,120 @@ #ifndef Telegraph_TGPeerIdAdapter_h #define Telegraph_TGPeerIdAdapter_h -static inline bool TGPeerIdIsGroup(int64_t peerId) { - return peerId < 0 && peerId > INT32_MIN; +// Namespace constants based on Swift implementation +#define TG_NAMESPACE_MASK 0x7 +#define TG_NAMESPACE_EMPTY 0x0 +#define TG_NAMESPACE_CLOUD 0x1 +#define TG_NAMESPACE_GROUP 0x2 +#define TG_NAMESPACE_CHANNEL 0x3 +#define TG_NAMESPACE_SECRET_CHAT 0x4 +#define TG_NAMESPACE_ADMIN_LOG 0x5 +#define TG_NAMESPACE_AD 0x6 +#define TG_NAMESPACE_MAX 0x7 + +// Helper functions for bit manipulation +static inline uint32_t TGPeerIdGetNamespace(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + return (uint32_t)((data >> 32) & TG_NAMESPACE_MASK); +} + +static inline int64_t TGPeerIdGetId(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + uint64_t idHighBits = (data >> (32 + 3)) << 32; + uint64_t idLowBits = data & 0xffffffff; + return (int64_t)(idHighBits | idLowBits); +} + +static inline int64_t TGPeerIdMake(uint32_t namespaceId, int64_t id) { + uint64_t data = 0; + uint64_t idBits = (uint64_t)id; + uint64_t idLowBits = idBits & 0xffffffff; + uint64_t idHighBits = (idBits >> 32) & 0xffffffff; + + data |= ((uint64_t)(namespaceId & TG_NAMESPACE_MASK)) << 32; + data |= (idHighBits << (32 + 3)); + data |= idLowBits; + + return (int64_t)data; +} + +// Updated peer type checks +static inline bool TGPeerIdIsEmpty(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_EMPTY; } static inline bool TGPeerIdIsUser(int64_t peerId) { - return peerId > 0 && peerId < INT32_MAX; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CLOUD; +} + +static inline bool TGPeerIdIsGroup(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_GROUP; } static inline bool TGPeerIdIsChannel(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 2 && peerId > ((int64_t)INT32_MIN) * 3; -} - -static inline bool TGPeerIdIsAdminLog(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 3 && peerId > ((int64_t)INT32_MIN) * 4; -} - -static inline bool TGPeerIdIsAd(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 4 && peerId > ((int64_t)INT32_MIN) * 5; -} - -static inline int32_t TGChannelIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsChannel(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 2 - peerId); - } else { - return 0; - } -} - -static inline int64_t TGPeerIdFromChannelId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 2 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromAdminLogId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 3 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromAdId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 4 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromGroupId(int32_t groupId) { - return -groupId; -} - -static inline int32_t TGGroupIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsGroup(peerId)) { - return (int32_t)-peerId; - } else { - return 0; - } -} - -static inline int32_t TGAdminLogIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsAdminLog(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 3 - peerId); - } else { - return 0; - } -} - -static inline int32_t TGAdIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsAd(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 4 - peerId); - } else { - return 0; - } + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CHANNEL; } static inline bool TGPeerIdIsSecretChat(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) && peerId > ((int64_t)INT32_MIN) * 2; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_SECRET_CHAT; +} + +static inline bool TGPeerIdIsAdminLog(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_ADMIN_LOG; +} + +static inline bool TGPeerIdIsAd(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_AD; +} + +// Conversion functions +static inline int64_t TGPeerIdFromUserId(int64_t userId) { + return TGPeerIdMake(TG_NAMESPACE_CLOUD, userId); +} + +static inline int64_t TGPeerIdFromGroupId(int64_t groupId) { + return TGPeerIdMake(TG_NAMESPACE_GROUP, groupId); +} + +static inline int64_t TGPeerIdFromChannelId(int64_t channelId) { + return TGPeerIdMake(TG_NAMESPACE_CHANNEL, channelId); +} + +static inline int64_t TGPeerIdFromSecretChatId(int64_t secretChatId) { + return TGPeerIdMake(TG_NAMESPACE_SECRET_CHAT, secretChatId); +} + +static inline int64_t TGPeerIdFromAdminLogId(int64_t adminLogId) { + return TGPeerIdMake(TG_NAMESPACE_ADMIN_LOG, adminLogId); +} + +static inline int64_t TGPeerIdFromAdId(int64_t adId) { + return TGPeerIdMake(TG_NAMESPACE_AD, adId); +} + +// Extract IDs +static inline int64_t TGUserIdFromPeerId(int64_t peerId) { + return TGPeerIdIsUser(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGGroupIdFromPeerId(int64_t peerId) { + return TGPeerIdIsGroup(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGChannelIdFromPeerId(int64_t peerId) { + return TGPeerIdIsChannel(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGSecretChatIdFromPeerId(int64_t peerId) { + return TGPeerIdIsSecretChat(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdminLogIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAdminLog(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAd(peerId) ? TGPeerIdGetId(peerId) : 0; } #endif diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoEditorInterfaceAssets.h index 1c10e40842..2418f1f7f4 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 be3bd2aa1d..64cb80fe1a 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 b6343a64a5..25bd2afc1b 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 5de1bfc9eb..fe801293be 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 38a614d6b2..3086f3f4e4 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditor.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditor.m @@ -551,6 +551,7 @@ self.trimStartValue = videoAdjustments.trimStartValue; self.trimEndValue = videoAdjustments.trimEndValue; self.sendAsGif = videoAdjustments.sendAsGif; + self.sendAsTelescope = videoAdjustments.sendAsTelescope; self.preset = videoAdjustments.preset; } @@ -581,13 +582,13 @@ 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 3257dd4b08..2cef6743d2 100644 --- a/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m +++ b/submodules/LegacyComponents/Sources/PGPhotoEditorValues.m @@ -13,6 +13,7 @@ @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 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 @@ 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 c751629564..4e733b9c9a 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1480,7 +1480,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus TGCameraCapturedPhoto *photo = (TGCameraCapturedPhoto *)editableItem; CGSize size = photo.originalSize; CGFloat height = size.width * 0.704f; - PGPhotoEditorValues *values = [PGPhotoEditorValues editorValuesWithOriginalSize:size cropRect:CGRectMake(0, floor((size.height - height) / 2.0f), size.width, height) cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0f cropMirrored:false toolValues:nil paintingData:nil sendAsGif:false]; + PGPhotoEditorValues *values = [PGPhotoEditorValues editorValuesWithOriginalSize:size cropRect:CGRectMake(0, floor((size.height - height) / 2.0f), size.width, height) cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0f cropMirrored:false toolValues:nil paintingData:nil sendAsGif:false sendAsTelescope:false]; SSignal *cropSignal = [[photo originalImageSignal:0.0] map:^UIImage *(UIImage *image) { @@ -1537,7 +1537,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus }]; bool hasCamera = !self.inhibitMultipleCapture && (((_intent == TGCameraControllerGenericIntent || _intent == TGCameraControllerGenericPhotoOnlyIntent || _intent == TGCameraControllerGenericVideoOnlyIntent) && !_shortcut) || (_intent == TGCameraControllerPassportMultipleIntent)); - 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 hasCoverButton: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 hasCoverButton: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 bf0ca5e06f..96decae1e4 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -482,7 +482,7 @@ } 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; @@ -964,7 +964,7 @@ id adjustments = [editingContext adjustmentsForItem:asset]; if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]]) { TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments; - if (videoAdjustments.sendAsGif) { + if (videoAdjustments.sendAsGif || videoAdjustments.sendAsTelescope) { grouping = false; } } @@ -1495,7 +1495,7 @@ 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 6144b3c4f0..cc4160d263 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -86,6 +86,7 @@ TGPhotoCaptionInputMixin *_captionMixin; TGModernButton *_muteButton; + TGModernButton *_telescopeButton; TGCheckButtonView *_checkButton; bool _ignoreSetSelected; TGMediaPickerPhotoCounterButton *_photoCounterButton; @@ -122,6 +123,8 @@ id _context; bool _ignoreSelectionUpdates; + bool _canSendTelescope; + bool _canShowTelescope; } @property (nonatomic, strong) ASHandle *actionHandle; @@ -133,7 +136,7 @@ @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 hasCoverButton:(bool)hasCoverButton +- (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 hasCoverButton:(bool)hasCoverButton { self = [super initWithFrame:CGRectZero]; if (self != nil) @@ -206,6 +209,9 @@ [[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"]; }; + _canSendTelescope = canSendTelescope; + _canShowTelescope = canShowTelescope; + _muteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0, 0, 40.0f, 40.0f)]; _muteButton.hidden = true; _muteButton.adjustsImageWhenHighlighted = false; @@ -214,7 +220,22 @@ [_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) { @@ -511,7 +532,8 @@ } 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; @@ -686,6 +708,10 @@ } strongSelf->_muteButton.hidden = !sendableAsGif; + if (strongSelf->_canShowTelescope) { + strongSelf->_telescopeButton.hidden = !sendableAsGif; + } + bool canHaveCover = false; if ([strongItemView isKindOfClass:[TGMediaPickerGalleryVideoItemView class]]) { TGMediaPickerGalleryVideoItemView *itemView = (TGMediaPickerGalleryVideoItemView *)strongItemView; @@ -1096,6 +1122,7 @@ TGPhotoEditorTab disabledButtons = TGPhotoEditorNoneTab; _muteButton.selected = adjustments.sendAsGif; + _telescopeButton.selected = adjustments.sendAsTelescope; TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; if (qualityButton != nil) @@ -1158,7 +1185,7 @@ }); } - if (adjustments.sendAsGif) + if (adjustments.sendAsGif || adjustments.sendAsTelescope) disabledButtons |= TGPhotoEditorQualityTab; [_portraitToolbarView setEditButtonsHighlighted:highlightedButtons]; @@ -1291,6 +1318,7 @@ { _checkButton.alpha = alpha; _muteButton.alpha = alpha; + _telescopeButton.alpha = alpha; _coverButton.alpha = alpha; _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; @@ -1300,6 +1328,7 @@ { _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; + _telescopeButton.userInteractionEnabled = !hidden; _coverButton.userInteractionEnabled = !hidden; } }]; @@ -1322,7 +1351,10 @@ _checkButton.userInteractionEnabled = !hidden; _muteButton.alpha = alpha; - _muteButton.userInteractionEnabled = !hidden; + _muteButton.userInteractionEnabled = !hidden; + + _telescopeButton.alpha = alpha; + _telescopeButton.userInteractionEnabled = !hidden; _coverButton.alpha = alpha; _coverButton.userInteractionEnabled = !hidden; @@ -1356,6 +1388,7 @@ { _checkButton.alpha = alpha; _muteButton.alpha = alpha; + _telescopeButton.alpha = alpha; _coverButton.alpha = alpha; _arrowView.alpha = alpha * 0.6; _recipientLabel.alpha = alpha * 0.6; @@ -1369,6 +1402,7 @@ { _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; + _telescopeButton.userInteractionEnabled = !hidden; _coverButton.userInteractionEnabled = !hidden; _portraitToolbarView.userInteractionEnabled = !hidden; _landscapeToolbarView.userInteractionEnabled = !hidden; @@ -1395,7 +1429,10 @@ _checkButton.userInteractionEnabled = !hidden; _muteButton.alpha = alpha; - _muteButton.userInteractionEnabled = !hidden; + _muteButton.userInteractionEnabled = !hidden; + + _telescopeButton.alpha = alpha; + _telescopeButton.userInteractionEnabled = !hidden; _coverButton.alpha = alpha; _coverButton.userInteractionEnabled = !hidden; @@ -1478,6 +1515,19 @@ [(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]; @@ -1659,6 +1709,7 @@ if (view == _photoCounterButton || view == _checkButton || view == _muteButton + || view == _telescopeButton || view == _groupButton || view == _cameraButton || view == _coverButton @@ -1739,6 +1790,37 @@ 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] - 26 - _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; @@ -2041,6 +2123,7 @@ } _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]; _coverButton.frame = [self _coverButtonFrameForOrientation:orientation screenEdges:screenEdges hasHeaderView:hasHeaderView]; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index a65545ddab..c213291682 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -40,6 +40,8 @@ NSString *_recipientName; bool _hasCamera; bool _isScheduledMessages; + bool _canShowTelescope; + bool _canSendTelescope; bool _hasCoverButton; } @@ -49,7 +51,7 @@ @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 hasCoverButton:(bool)hasCoverButton +- (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 hasCoverButton:(bool)hasCoverButton { self = [super init]; if (self != nil) @@ -71,6 +73,8 @@ _recipientName = recipientName; _hasCamera = hasCamera; _isScheduledMessages = isScheduledMessages; + _canSendTelescope = canSendTelescope; + _canShowTelescope = canShowTelescope; _hasCoverButton = hasCoverButton; __weak TGMediaPickerGalleryModel *weakSelf = self; @@ -181,7 +185,7 @@ 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 hasCoverButton:_hasCoverButton]; + _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 hasCoverButton:_hasCoverButton]; _interfaceView.hasCaptions = _hasCaptions; _interfaceView.allowCaptionEntities = _allowCaptionEntities; _interfaceView.hasTimer = _hasTimer; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m index 7b6117cf92..745f1dc1c3 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryPhotoItemView.m @@ -416,7 +416,7 @@ 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 581a462475..2ffd7fe102 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -116,6 +116,7 @@ bool _downloaded; bool _sendAsGif; + bool _sendAsTelescope; bool _autoplayed; CMTime _chaseTime; @@ -516,6 +517,7 @@ id 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])) @@ -1563,7 +1565,8 @@ } 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]]]; @@ -1597,6 +1600,90 @@ [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) @@ -1656,7 +1743,7 @@ 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 756f4deb62..7f8084a54d 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -84,7 +84,7 @@ 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 hasCoverButton:hasCoverButton]; + 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 hasCoverButton:hasCoverButton]; _galleryModel = model; model.stickersContext = stickersContext; model.inhibitMute = inhibitMute; diff --git a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m index 15189ec41f..de36e73f7d 100644 --- a/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m +++ b/submodules/LegacyComponents/Sources/TGMediaVideoConverter.m @@ -131,7 +131,7 @@ 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 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, 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 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, case TGMediaVideoConversionPresetVideoMessage: return 1000; + + case TGMediaVideoConversionPresetVideoMessageHD: + return 2000; case TGMediaVideoConversionPresetProfile: return 1500; @@ -1453,6 +1459,9 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, 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 ac683a46ce..87cb32a11c 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m @@ -145,6 +145,16 @@ 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 14af60512a..de2ff28eff 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -178,7 +178,7 @@ 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 hasCoverButton: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 hasCoverButton:false]; model.controller = galleryController; model.stickersContext = stickersContext; @@ -297,7 +297,7 @@ } 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 ffa4c1eb68..7d5b2aeba3 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; @synthesize cropMirrored = _cropMirrored; @synthesize paintingData = _paintingData; @synthesize sendAsGif = _sendAsGif; +@synthesize sendAsTelescope = _sendAsTelescope; @synthesize toolValues = _toolValues; + (instancetype)editAdjustmentsWithOriginalSize:(CGSize)originalSize @@ -38,6 +40,7 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; adjustments->_toolValues = toolValues; adjustments->_paintingData = paintingData; adjustments->_sendAsGif = sendAsGif; + adjustments->_sendAsTelescope = sendAsTelescope; adjustments->_preset = preset; if (trimStartValue > trimEndValue) @@ -86,6 +90,8 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; } 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; adjustments->_preset = preset; if (preset == TGMediaVideoConversionPresetAnimation) adjustments->_sendAsGif = true; + if (preset == TGMediaVideoConversionPresetVideoMessage || preset == TGMediaVideoConversionPresetVideoMessageHD) + adjustments->_sendAsTelescope = true; return adjustments; } @@ -140,6 +148,7 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; } dict[@"sendAsGif"] = @(self.sendAsGif); + dict[@"sendAsTelescope"] = @(self.sendAsTelescope); if (self.preset != TGMediaVideoConversionPresetCompressedDefault) dict[@"preset"] = @(self.preset); @@ -463,6 +477,9 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; 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 d05b594a21..6f4825687a 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -66,6 +66,7 @@ typedef enum SQueue *_queue; AVCaptureDevicePosition _preferredPosition; + bool _startWithRearCam; TGVideoCameraPipeline *_capturePipeline; NSURL *_url; @@ -150,8 +151,8 @@ typedef enum @end @implementation TGVideoMessageCaptureController - -- (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 +# 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 @@ typedef enum _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 @@ typedef enum 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 @@ typedef enum 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 cfae8f3fe5..5d1f3092a8 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 c88ee2e026..71875270a5 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 @@ -404,7 +405,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) @@ -765,6 +767,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A break } case let .video(data, thumbnail, cover, adjustments, caption, asFile, asAnimation, stickers): + var adjustments = adjustments var finalDimensions: CGSize var finalDuration: Double switch data { @@ -839,8 +842,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? @@ -918,7 +990,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: [], videoCover: videoCover, 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/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index e1d22b21f4..2a05499434 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -468,7 +468,7 @@ open class LegacyController: ViewController, PresentableController { fatalError("init(coder:) has not been implemented") } - public func bind(controller: UIViewController) { + open func bind(controller: UIViewController) { self.legacyController = controller if let controller = controller as? TGViewController { controller.customRemoveFromParentViewController = { [weak self] in diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 37b56715f8..1aed44185d 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -826,7 +826,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 b0f3f832fe..636f28dfa8 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 0f2efd4372..4077c27fdf 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/LottieCpp/lottiecpp b/submodules/LottieCpp/lottiecpp index 4a3144b5d5..b885e63e76 160000 --- a/submodules/LottieCpp/lottiecpp +++ b/submodules/LottieCpp/lottiecpp @@ -1 +1 @@ -Subproject commit 4a3144b5d527429f7bbd0f07003cb372bf8939ce +Subproject commit b885e63e766890d1cbf36b66cfe27cca55a6ec90 diff --git a/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift b/submodules/Media/LocalAudioTranscription/Sources/LocalAudioTranscription.swift index 243b6220da..d7736be030 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 { @@ -49,6 +50,7 @@ private func transcribeAudio(path: String, locale: String) -> Signal 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 507e1d781f..6d7c0f2ef6 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -144,8 +144,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, hasCoverButton: hasCoverButton)! + 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, hasCoverButton: hasCoverButton)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 5d848b66c9..c8415ba70d 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 @@ -607,8 +608,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att useModernCamera = true } - if useLegacyCamera { - let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true + if useLegacyCamera && !SGSimpleSettings.shared.disableGalleryCamera { + let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true && !SGSimpleSettings.shared.disableGalleryCameraPreview let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)! cameraView.clipsToBounds = true @@ -631,7 +632,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.gridNode.scrollView.addSubview(cameraView) self.gridNode.addSubnode(self.cameraActivateAreaNode) - } else if useModernCamera, !Camera.isIpad { + } else if useModernCamera, !Camera.isIpad, !SGSimpleSettings.shared.disableGalleryCamera { #if !targetEnvironment(simulator) var cameraPosition: Camera.Position = .back if case .assets(nil, .createAvatar) = controller.subject { @@ -751,14 +752,18 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att let isCameraActive = !self.isSuspended && !self.hasGallery && self.isCameraPreviewVisible if let cameraView = self.cameraView { if isCameraActive { - cameraView.resumePreview() + if !SGSimpleSettings.shared.disableGalleryCameraPreview { + cameraView.resumePreview() + } } else { cameraView.pausePreview() } } else if let camera = self.modernCamera, let cameraView = self.modernCameraView { if isCameraActive { - cameraView.isEnabled = true - camera.startCapture() + if !SGSimpleSettings.shared.disableGalleryCameraPreview { + cameraView.isEnabled = true + camera.startCapture() + } } else { cameraView.isEnabled = false camera.stopCapture() diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index af588856e6..a2597a2264 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 f377d9024c..15d20bd4a9 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 68e2f921ed..448a34e8d4 100644 --- a/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m +++ b/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m @@ -302,7 +302,7 @@ MTAtomic *sharedFetchConfigKeychains() { 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 6f26b62be7..c346aa6dea 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -231,7 +231,7 @@ static int32_t fixedTimeDifferenceValue = 0; 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"); @@ -247,6 +247,7 @@ static int32_t fixedTimeDifferenceValue = 0; _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 80c1bef6d3..dcedab0de7 100644 --- a/submodules/MtProtoKit/Sources/MTProxyConnectivity.m +++ b/submodules/MtProtoKit/Sources/MTProxyConnectivity.m @@ -64,7 +64,7 @@ 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 e8cf239881..416fedc2d2 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -760,6 +760,8 @@ struct ctr_state { NSMutableArray *_pendingDataQueue; NSMutableData *_receivedDataBuffer; MTTcpReceiveData *_pendingReceiveData; + + bool _forceLocalDNS; } @property (nonatomic) int64_t packetHeadDecodeToken; @@ -850,6 +852,8 @@ struct ctr_state { _pendingDataQueue = [[NSMutableArray alloc] init]; _receivedDataBuffer = [[NSMutableData alloc] init]; + + _forceLocalDNS = context.forceLocalDNS; } return self; } @@ -920,7 +924,7 @@ struct ctr_state { 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 @@ struct ctr_state { 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 3d34a6ef96..85fc70ac00 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/PeerInfoUI/Sources/PeerBanTimeoutController.swift b/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift index 396e10a4d6..d43df870f9 100644 --- a/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift +++ b/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift @@ -68,7 +68,7 @@ private final class PeerBanTimeoutActionSheetItem: ActionSheetItem { init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { self.strings = strings - self.currentValue = roundDateToDays(currentValue) + self.currentValue = /*roundDateToDays(*/currentValue/*)*/ self.valueChanged = valueChanged } @@ -96,8 +96,8 @@ private final class PeerBanTimeoutActionSheetItemNode: ActionSheetItemNode { self.pickerView = UIDatePicker() self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .date - self.pickerView.date = Date(timeIntervalSince1970: Double(roundDateToDays(currentValue))) + self.pickerView.datePickerMode = .dateAndTime + self.pickerView.date = Date(timeIntervalSince1970: Double(/*roundDateToDays(*/currentValue/*)*/)) self.pickerView.locale = localeWithStrings(strings) self.pickerView.minimumDate = Date() self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) @@ -122,6 +122,6 @@ private final class PeerBanTimeoutActionSheetItemNode: ActionSheetItemNode { } @objc private func datePickerUpdated() { - self.valueChanged(roundDateToDays(Int32(self.pickerView.date.timeIntervalSince1970))) + self.valueChanged(/*roundDateToDays(*/Int32(self.pickerView.date.timeIntervalSince1970)/*)*/) } } diff --git a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift index cdafe9a37c..38d409501f 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 702394b8d0..f06bb2dee1 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 6254850f9e..1a2339e5c9 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 f01f636801..5a9ec9c820 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -2425,7 +2425,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) @@ -2434,7 +2435,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 00a0a4caa5..57777f8354 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 885dac1f52..ffc3d61aac 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 5d3c60997c..1f64ffe8e4 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 @@ -886,6 +887,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 } @@ -983,6 +990,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 c96c0668d6..3b2b234ac0 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 @@ -2163,7 +2164,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 { @@ -3096,7 +3097,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let presentationData = self.screenContext.presentationData + + // MARK: Swiftgram + 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 { guard let context = self.screenContext.context else { return @@ -3277,7 +3283,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.updateInProgress(false) self.updated(transition: .immediate) } - }) + })*/ } func updateIsFocused(_ isFocused: Bool) { @@ -3686,9 +3692,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } else 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 controller = environment.controller diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 1aefe3e225..9e08a1250e 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 6dc53f1ccc..f126c8a769 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 7a5a342bc9..7260909f91 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 5048850c0b..5bb5b80fc8 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/BUILD b/submodules/SSignalKit/SwiftSignalKit/BUILD index b3c12dbee1..e4f8687cd7 100644 --- a/submodules/SSignalKit/SwiftSignalKit/BUILD +++ b/submodules/SSignalKit/SwiftSignalKit/BUILD @@ -1,9 +1,13 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgsrc = [ + "//Swiftgram/SGSwiftSignalKit:SGSwiftSignalKit" +] + swift_library( name = "SwiftSignalKit", module_name = "SwiftSignalKit", - srcs = glob([ + srcs = sgsrc + glob([ "Source/**/*.swift", ]), copts = [ diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 675ccef464..e753e9b512 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/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index 48e42c7283..0cfd17e141 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -364,6 +364,12 @@ public final class SelectablePeerNode: ASDisplayNode { } self.setNeedsLayout() + self.isAccessibilityElement = true + self.accessibilityLabel = customTitle ?? text + self.accessibilityTraits = [.button] + if self.currentSelected { + self.accessibilityTraits.insert(.selected) + } } public func updateSelection(selected: Bool, animated: Bool) { @@ -458,6 +464,12 @@ public final class SelectablePeerNode: ASDisplayNode { } self.setNeedsLayout() } + + if selected { + self.accessibilityTraits.insert(.selected) + } else { + self.accessibilityTraits.remove(.selected) + } } override public func didLoad() { diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index edf5e5466d..282e189cc1 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 2caf610c6a..76e359c98c 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 48ea6da9db..bc5b7a96fe 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 7807536eee..166b144948 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 88351caec7..92fbfe8113 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -161,10 +161,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 6ec1448740..f847ea92a8 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 784a9b396d..6f47806d8c 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 1bc481e1dd..c36469ec82 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 e22d5e2f17..8bb9bb0c45 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 946e81f609..3f74ca3647 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -357,6 +357,55 @@ 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 "SGPro": + name = "Pro" + bordered = false + case "SGGold": + name = "Gold" + bordered = false + case "SGDucky": + name = "Ducky" + bordered = false + case "SGDay": + name = "Day" + bordered = false case "BlueIcon": name = item.strings.Appearance_AppIconDefault case "BlackIcon": @@ -387,7 +436,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { name = icon.name } - imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { + imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isSGPro, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { item.updated(icon) }) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 7dfc024c33..8c599bc20b 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -567,6 +567,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The controller?.replace(with: c) } pushControllerImpl?(controller) + // MARK: Swiftgram + } else if icon.isSGPro && context.sharedContext.immediateSGStatus.status < 2 { + if let payWallController = context.sharedContext.makeSGPayWallController(context: context) { + presentControllerImpl?(payWallController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil) + } } else { currentAppIconName.set(icon.name) context.sharedContext.applicationBindings.requestSetAlternateIconName(icon.name, { _ in @@ -1027,12 +1034,14 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings, ApplicationSpecificSharedDataKeys.sgStatus]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings - let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false + // MARK: Swiftgram + let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default + let isPremium = sgStatus.status > 1 let themeReference: PresentationThemeReference if presentationData.autoNightModeTriggered { diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index e510451915..ec5db66c83 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 be1a0091b5..fad5891c61 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 @@ -441,7 +442,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), @@ -1048,7 +1056,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/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index b3046db256..8624e0127f 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -741,6 +741,40 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil { self.setActionNodesHidden(false, inputField: true, actions: true, animated: false) } + + // MARK: Swiftgram + // Replace your current accessibility setup with this: + self.isAccessibilityElement = false + self.accessibilityViewIsModal = true + self.shouldGroupAccessibilityChildren = false + + // Make dim node not accessible + self.dimNode.isAccessibilityElement = false + + // Wrapping scroll node setup + self.wrappingScrollNode.isAccessibilityElement = false + self.wrappingScrollNode.accessibilityViewIsModal = true + self.wrappingScrollNode.shouldGroupAccessibilityChildren = true + + // Content container setup + self.contentContainerNode.isAccessibilityElement = false + self.contentContainerNode.accessibilityViewIsModal = true + self.contentContainerNode.shouldGroupAccessibilityChildren = true + self.contentContainerNode.accessibilityLabel = self.presentationData.strings.BoostGift_SelectRecipients + + // Cancel button setup + self.cancelButtonNode.isAccessibilityElement = true + self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Cancel + self.cancelButtonNode.accessibilityTraits = .button + + // Action button setup + self.actionButtonNode.isAccessibilityElement = true + self.actionButtonNode.accessibilityLabel = "Send" + self.actionButtonNode.accessibilityTraits = .button + + // Input field setup + self.inputFieldNode.isAccessibilityElement = true + self.inputFieldNode.accessibilityLabel = "Comment" } deinit { @@ -753,6 +787,13 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } + + // Make the container view trap accessibility focus + self.view.accessibilityViewIsModal = true + self.wrappingScrollNode.view.accessibilityViewIsModal = true + + // If needed, set a label for VoiceOver + self.view.accessibilityLabel = "Share with" } func transitionToPeerTopics(_ peer: EngineRenderedPeer) { diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 6680872509..d35feabedc 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 @@ -316,6 +317,30 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } self.contentTitleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTapGesture(_:)))) + + // MARK: Swiftgram + self.isAccessibilityElement = false + + self.contentTitleNode.isAccessibilityElement = true + self.contentTitleNode.accessibilityLabel = strings.ShareMenu_ShareTo + self.contentTitleNode.accessibilityTraits = .header + + self.contentSubtitleNode.isAccessibilityElement = true + self.contentSubtitleNode.accessibilityLabel = strings.ShareMenu_SelectChats + + self.searchButtonNode.isAccessibilityElement = true + self.searchButtonNode.accessibilityLabel = strings.Common_Search + self.searchButtonNode.accessibilityTraits = .button + + self.shareButtonNode.isAccessibilityElement = true + self.shareButtonNode.accessibilityLabel = "System Share Menu" + self.shareButtonNode.accessibilityTraits = .button + + self.contentTitleAccountNode.isAccessibilityElement = true + self.contentTitleAccountNode.accessibilityLabel = strings.Shortcut_SwitchAccount + self.contentTitleAccountNode.accessibilityTraits = .button + // + } deinit { @@ -691,6 +716,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { }) } self.contentSubtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.actionSheet.secondaryTextColor) + self.contentSubtitleNode.accessibilityLabel = subtitleText } self.contentGridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ShareControllerPeerGridItemNode { diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index d063a8e0cf..e3a398ce3e 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 2c586b59b0..91ab60f023 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -450,12 +450,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 1abbce2193..7ba7bfd09d 100644 --- a/submodules/TabBarUI/BUILD +++ b/submodules/TabBarUI/BUILD @@ -1,15 +1,23 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", +] + +sgsrc = [ + "//Swiftgram/SGTabBarHeightModifier:SGTabBarHeightModifier" +] + swift_library( name = "TabBarUI", module_name = "TabBarUI", - srcs = glob([ + srcs = sgsrc + glob([ "Sources/**/*.swift", ]), 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 d0b8d37680..b67ecb19e6 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 @@ -11,6 +12,7 @@ private extension ToolbarTheme { final class TabBarControllerNode: ASDisplayNode { private var navigationBarPresentationData: NavigationBarPresentationData + private let showTabNames: Bool // MARK: Swiftgram private var theme: TabBarControllerTheme let tabBarNode: TabBarNode private let disabledOverlayNode: ASDisplayNode @@ -42,10 +44,12 @@ 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.showTabNames = showTabNames 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 @@ -90,7 +94,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 @@ -101,8 +105,10 @@ final class TabBarControllerNode: ASDisplayNode { let bottomInset: CGFloat = layout.insets(options: options).bottom if !layout.safeInsets.left.isZero { tabBarHeight = 34.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: true) // MARK: Swiftgram } else { tabBarHeight = 49.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: false) // MARK: Swiftgram } let tabBarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (self.tabBarHidden ? 0.0 : tabBarHeight)), size: CGSize(width: layout.size.width, height: tabBarHeight)) diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 0d9b77d929..e4b63e58f2 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 { @@ -264,8 +269,10 @@ open class TabBarControllerImpl: ViewController, TabBarController { let bottomInset: CGFloat = validLayout.insets(options: options).bottom if !validLayout.safeInsets.left.isZero { tabBarHeight = 34.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: strongSelf.showTabNames, tabBarHeight: tabBarHeight, layout: validLayout, defaultBarSmaller: true) // MARK: Swiftgram } else { tabBarHeight = 49.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: strongSelf.showTabNames, tabBarHeight: tabBarHeight, layout: validLayout, defaultBarSmaller: false) // MARK: Swiftgram } updatedLayout.intrinsicInsets.bottom = tabBarHeight @@ -443,8 +450,10 @@ open class TabBarControllerImpl: ViewController, TabBarController { let bottomInset: CGFloat = updatedLayout.insets(options: options).bottom if !updatedLayout.safeInsets.left.isZero { tabBarHeight = 34.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: true) // MARK: Swiftgram } else { tabBarHeight = 49.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: false) // MARK: Swiftgram } if !self.tabBarControllerNode.tabBarHidden { updatedLayout.intrinsicInsets.bottom = tabBarHeight @@ -472,8 +481,10 @@ open class TabBarControllerImpl: ViewController, TabBarController { let bottomInset: CGFloat = updatedLayout.insets(options: options).bottom if !updatedLayout.safeInsets.left.isZero { tabBarHeight = 34.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: true) // MARK: Swiftgram } else { tabBarHeight = 49.0 + bottomInset + tabBarHeight = sgTabBarHeightModifier(showTabNames: self.showTabNames, tabBarHeight: tabBarHeight, layout: layout, defaultBarSmaller: false) // MARK: Swiftgram } if !self.tabBarControllerNode.tabBarHidden { updatedLayout.intrinsicInsets.bottom = tabBarHeight diff --git a/submodules/TabBarUI/Sources/TabBarNode.swift b/submodules/TabBarUI/Sources/TabBarNode.swift index b5d14b5460..592e86c350 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 319722988c..091fa7890b 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 78db670856..7f53a65d1f 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import SwiftSignalKit @@ -1063,6 +1064,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/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index fc1b19ed9d..aa256aacc4 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -48,8 +48,8 @@ private class MediaHeaderItemNode: ASDisplayNode { var subtitleString: NSAttributedString? if let playbackItem = playbackItem, let displayData = playbackItem.displayData { switch displayData { - case let .music(title, performer, _, long, _): - rateButtonHidden = !long + case let .music(title, performer, _, _, _): + rateButtonHidden = false let titleText: String = title ?? strings.MediaPlayer_UnknownTrack let subtitleText: String = performer ?? strings.MediaPlayer_UnknownArtist diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index ea7eb8b5e9..f0bd341b4c 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -40,6 +40,10 @@ apple_resource_bundle( ], ) +sgdeps = [ + "//Swiftgram/SGAppGroupIdentifier:SGAppGroupIdentifier" +] + swift_library( name = "TelegramCallsUI", module_name = "TelegramCallsUI", @@ -52,7 +56,7 @@ swift_library( data = [ ":TelegramCallsUIBundle", ], - deps = [ + deps = sgdeps + [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift index 323457dbb7..b064814715 100644 --- a/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift +++ b/submodules/TelegramCallsUI/Sources/CallKitIntegration.swift @@ -1,3 +1,4 @@ +import SGAppGroupIdentifier import Foundation import UIKit import CallKit @@ -20,7 +21,7 @@ public final class CallKitIntegration { return false #else if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - return Locale.current.regionCode?.lowercased() != "cn" + return Locale.current.regionCode?.lowercased() != "cn" && !(UserDefaults(suiteName: sgAppGroupIdentifier())?.bool(forKey: "legacyNotificationsFix") ?? false) } else { return false } @@ -149,7 +150,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 fe9bc15dbf..b7b8528b55 100644 --- a/submodules/TelegramCore/BUILD +++ b/submodules/TelegramCore/BUILD @@ -1,15 +1,28 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = [ + "//Swiftgram/SwiftSoup:SwiftSoup", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGTranslationLangFix:SGTranslationLangFix", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme", + "//Swiftgram/SGConfig:SGConfig", + "//Swiftgram/SGLogging:SGLogging", +] + +sgsrc = [ + "//Swiftgram/SGIQTP:SGIQTP", +] + swift_library( name = "TelegramCore", module_name = "TelegramCore", - srcs = glob([ + srcs = sgsrc + glob([ "Sources/**/*.swift", ]), 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 180790f157..bab4c747f5 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -215,6 +215,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 27ee6e65f7..69f876cffb 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 702c560994..dd05dd0908 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 @@ -388,9 +389,9 @@ private final class FetchImpl { } if isStory { - self.defaultPartSize = 512 * 1024 + self.defaultPartSize = getSGDownloadPartSize(512 * 1024, fileSize: self.size) } else { - self.defaultPartSize = 128 * 1024 + self.defaultPartSize = getSGDownloadPartSize(128 * 1024, fileSize: self.size) } self.cdnPartSize = 128 * 1024 @@ -440,7 +441,7 @@ private final class FetchImpl { maxPartSize: 1 * 1024 * 1024, partAlignment: 4 * 1024, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6, + maxPendingParts: getSGMaxPendingParts(6), decryptionState: decryptionState )) } @@ -696,7 +697,7 @@ private final class FetchImpl { maxPartSize: self.cdnPartSize * 2, partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6, + maxPendingParts: getSGMaxPendingParts(6), decryptionState: nil )) self.update() @@ -745,7 +746,7 @@ private final class FetchImpl { maxPartSize: self.defaultPartSize, partAlignment: 4 * 1024, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6, + maxPendingParts: getSGMaxPendingParts(6), decryptionState: nil )) @@ -931,7 +932,7 @@ private final class FetchImpl { maxPartSize: self.cdnPartSize * 2, partAlignment: self.cdnPartSize, partDivision: 1 * 1024 * 1024, - maxPendingParts: 6, + maxPendingParts: getSGMaxPendingParts(6), decryptionState: nil )) case let .cdnRefresh(cdnData, refreshToken): diff --git a/submodules/TelegramCore/Sources/Network/MultipartUpload.swift b/submodules/TelegramCore/Sources/Network/MultipartUpload.swift index 3f07e3bb5e..d72858ed3a 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 525a743eb7..4f497949e3 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 @@ -504,8 +507,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 8c1cf3caaf..42579aa883 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -574,11 +574,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)) } } @@ -703,10 +703,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))) } } @@ -714,6 +721,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 204fc79900..f556290531 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 7b081fb90a..4e36d2cf8f 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 bb88bb98c9..9a4753ce81 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import Postbox import SwiftSignalKit @@ -207,3 +208,56 @@ 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) + SGSimpleSettings.shared.dismissedSGSuggestions.append(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) && !SGSimpleSettings.shared.dismissedSGSuggestions.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 fa04d5db67..32babb772d 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 5239cae769..5142086c57 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 838c889580..b8d2156bb8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Localization/LocalizationInfo.swift @@ -24,3 +24,18 @@ 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/"), + // TODO(swiftgram): Japanese beta + // 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/"), + LocalizationInfo(languageCode: "ja-raw", baseLanguageCode: "ja-raw", customPluralizationCode: "ja", title: "Japanese", localizedTitle: "日本語 (beta)", isOfficial: true, totalStringCount: 9697, translatedStringCount: 9683, platformUrl: "https://translations.telegram.org/ja/"), + // 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/RequestChatContextResults.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift index 05dc2ec2b0..91bc2884c3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestChatContextResults.swift @@ -1,3 +1,4 @@ +import SGLogging import Foundation import Postbox import SwiftSignalKit @@ -52,7 +53,7 @@ public struct RequestChatContextResultsResult { } } -func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { +func _internal_requestChatContextResults(IQTP: Bool = false, account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) { return (bot, peer) @@ -127,6 +128,10 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location) } |> mapError { error -> RequestChatContextResultsError in + // MARK: Swiftgram + if IQTP { + SGLogger.shared.log("SGIQTP", "Error requesting inline results: \(error.errorDescription ?? "nil")") + } if error.errorDescription == "BOT_INLINE_GEO_REQUIRED" { return .locationRequired } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index 83b1d94507..75ae60727f 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 913cf3573f..bf5ecdc4ba 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 @@ -75,6 +76,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 { @@ -361,8 +369,8 @@ public extension TelegramEngine { return _internal_updateStarsReactionPrivacy(account: self.account, messageId: id, privacy: privacy) } - public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { - return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) + public func requestChatContextResults(IQTP: Bool = false, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { + return _internal_requestChatContextResults(IQTP: IQTP, account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults) } public func removeRecentlyUsedHashtag(string: String) -> Signal { @@ -554,6 +562,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], fromLang: String?, toLang: String, generateEntitiesFunction: @escaping (String) -> [MessageTextEntity], enableLocalIfPossible: Bool) -> Signal { + return _internal_translateMessagesViaText(account: self.account, messagesDict: messagesDict, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible, generateEntitiesFunction: generateEntitiesFunction) + } public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal { return _internal_translateMessages(account: self.account, messageIds: messageIds, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: enableLocalIfPossible) @@ -1399,6 +1412,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 73ce8e2649..92eeda6307 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 @@ -174,7 +180,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } } 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") { @@ -254,6 +260,42 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } +func _internal_translateMessagesViaText(account: Account, messagesDict: [EngineMessage.Id: String], fromLang: String?, toLang: String, enableLocalIfPossible: Bool, 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 @@ -304,3 +346,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 1c9193d8bf..4dd2bc7313 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 @@ -1090,13 +1091,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 } @@ -1603,4 +1609,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 a2a3cc0197..bc685ce32d 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)" } } } @@ -270,8 +278,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/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 8ad523ddd1..3923b5a349 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -46,8 +46,10 @@ public struct PresentationAppIcon: Equatable { public let imageName: String public let isDefault: Bool public let isPremium: Bool + public let isSGPro: Bool - public init(name: String, imageName: String, isDefault: Bool = false, isPremium: Bool = false) { + public init(isSGPro: Bool = false, name: String, imageName: String, isDefault: Bool = false, isPremium: Bool = false) { + self.isSGPro = isSGPro self.name = name self.imageName = imageName self.isDefault = isDefault diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 2dea72f9a3..5238c6431e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -291,6 +291,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 2f97140cb7..b59c4c79e3 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 aa4b951618..a414a11cb0 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -63,6 +63,8 @@ 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 swiftgramPro = renderIcon(name: "SwiftgramPro", scaleFactor: 30.0 / 256.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/BUILD b/submodules/TelegramStringFormatting/BUILD index 932916cf5c..2180b8a0d0 100644 --- a/submodules/TelegramStringFormatting/BUILD +++ b/submodules/TelegramStringFormatting/BUILD @@ -1,5 +1,7 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgdeps = ["//Swiftgram/SGSimpleSettings:SGSimpleSettings"] + swift_library( name = "TelegramStringFormatting", module_name = "TelegramStringFormatting", @@ -9,7 +11,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//submodules/TelegramCore:TelegramCore", "//submodules/Display:Display", "//submodules/PlatformRestrictionMatching:PlatformRestrictionMatching", diff --git a/submodules/TelegramStringFormatting/Sources/DateFormat.swift b/submodules/TelegramStringFormatting/Sources/DateFormat.swift index 430fde43d6..a2d537a081 100644 --- a/submodules/TelegramStringFormatting/Sources/DateFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/DateFormat.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import TelegramPresentationData import TelegramUIPreferences @@ -46,8 +47,11 @@ public func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: Presenta } else { gmtime_r(&t, &timeinfo) } - - return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat) + if SGSimpleSettings.shared.secondsInMessages { + return stringForShortTimestampWithSeconds(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: timeinfo.tm_sec, dateTimeFormat: dateTimeFormat) + } else { + return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat) + } } public func getDateTimeComponents(timestamp: Int32) -> (day: Int32, month: Int32, year: Int32, hour: Int32, minutes: Int32) { @@ -193,3 +197,69 @@ 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) +} + +// MARK: Swiftgram +public func stringForShortTimestampWithSeconds(hours: Int32, minutes: Int32, seconds: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String { + switch dateTimeFormat.timeFormat { + case .regular: + let hourString: String + if hours == 0 { + hourString = "12" + } else if hours > 12 { + hourString = "\(hours - 12)" + } else { + hourString = "\(hours)" + } + + let periodString: String + if hours >= 12 { + periodString = "PM" + } else { + periodString = "AM" + } + + let minuteString: String + if minutes >= 10 { + minuteString = "\(minutes)" + } else { + minuteString = "0\(minutes)" + } + if seconds >= 10 { + return "\(hourString):\(minuteString):\(seconds)\u{00a0}\(periodString)" + } else { + return "\(hourString):\(minuteString):0\(seconds)\u{00a0}\(periodString)" + } + case .military: + return String(format: "%02d:%02d:%02d", arguments: [Int(hours), Int(minutes), Int(seconds)]) + } +} +// diff --git a/submodules/TelegramStringFormatting/Sources/Geo.swift b/submodules/TelegramStringFormatting/Sources/Geo.swift index cb065e12d7..9e4668e980 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 468bd87bfc..348568e9f8 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 e289c562b6..46ab3288bf 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 4c64d17ab7..2e3e895848 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -5,6 +5,31 @@ 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/SGInputToolbar:SGInputToolbar", + "//Swiftgram/SGIAP:SGIAP", + "//Swiftgram/SGPayWall:SGPayWall", + "//Swiftgram/SGProUI:SGProUI", + "//Swiftgram/SGKeychainBackupManager:SGKeychainBackupManager", + # "//Swiftgram/SGContentAnalysis:SGContentAnalysis" +] + +sgsrcs = [ + "//Swiftgram/SGDBReset:SGDBReset", + "//Swiftgram/SGShowMessageJson:SGShowMessageJson", + "//Swiftgram/ChatControllerImplExtension:ChatControllerImplExtension" +] filegroup( name = "TelegramUIResources", @@ -44,11 +69,11 @@ swift_library( module_name = "TelegramUI", srcs = glob([ "Sources/**/*.swift", - ]), + ]) + sgsrcs, copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgdeps + [ "//third-party/recaptcha:RecaptchaEnterprise", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/SSignalKit/SSignalKit:SSignalKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index dd2809fc38..65dd8bf3de 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? @@ -802,7 +805,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 @@ -834,7 +837,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 4f948fc37e..6412e75f99 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 a998ec96ff..149b858c68 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 @@ -126,7 +129,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 @@ -299,6 +302,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 } } @@ -1773,11 +1824,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 } @@ -2251,13 +2313,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) } @@ -4417,6 +4495,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 @@ -4586,6 +4680,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) @@ -4612,6 +4729,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) @@ -4750,18 +4890,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: @@ -5417,6 +5571,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)) { @@ -5811,6 +5969,83 @@ 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) + } + // TODO(swiftgram): pass fromLang + let _ = translateMessageIds(context: item.context, messageIds: [item.message.id], fromLang: nil, 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 { @@ -6071,6 +6306,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 87db0a7555..2eb7c11af5 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 5e9f63f0b6..2d4a3bdf80 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 @@ -1413,5 +1414,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 88c6dd9b03..270807d300 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -351,7 +351,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { return } - if !context.isPremium, case .inProgress = self.audioTranscriptionState { + if /*!context.isPremium,*/ case .inProgress = self.audioTranscriptionState { return } @@ -359,7 +359,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) { @@ -418,7 +419,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)) @@ -450,7 +451,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let result = result else { return .single(nil) } - return transcribeAudio(path: result, appLocale: appLocale) + + return transcribeAudio(path: result, appLocale: arguments.controllerInteraction.sgGetChatPredictedLang() ?? appLocale) } self.transcribeDisposable = (signal @@ -770,7 +772,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 { @@ -801,7 +804,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } let currentTime = Int32(Date().timeIntervalSince1970) - if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime { + if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime, { return false }() /* MARK: Swiftgram */ { updatedAudioTranscriptionState = .locked } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index f95da1047c..0d5048063e 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() @@ -1885,6 +1886,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 31db545b01..fa904ccafb 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 3938669792..1dbb050eab 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 @@ -946,6 +947,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 var isGift = false @@ -968,6 +971,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 @@ -1191,6 +1207,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 { @@ -2948,6 +2967,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 e704fe5675..95ffae0b7e 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 b76046a703..98d3c066e5 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 @@ -480,8 +482,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/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 346ac78203..267d4458c0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -1,3 +1,4 @@ +import SGSimpleSettings import Foundation import UIKit import AsyncDisplayKit @@ -663,6 +664,9 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { public var playedEffectAnimation: Bool = false public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] + private var wasFilteredKeywordTested: Bool = false + private var matchedFilterKeyword: String? = nil + public required init(rotated: Bool) { super.init(layerBacked: false, dynamicBounce: true, rotated: rotated) if rotated { @@ -683,10 +687,23 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { self.item = nil self.frame = CGRect() + self.wasFilteredKeywordTested = false + self.matchedFilterKeyword = nil } open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { self.item = item + + if !self.wasFilteredKeywordTested && !SGSimpleSettings.shared.messageFilterKeywords.isEmpty && SGSimpleSettings.shared.ephemeralStatus > 1 { + let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId) + if incomingMessage { + if let matchedKeyword = SGSimpleSettings.shared.messageFilterKeywords.first(where: { item.message.text.contains($0) }) { + self.matchedFilterKeyword = matchedKeyword + self.alpha = item.presentationData.theme.theme.overallDarkAppearance ? 0.2 : 0.3 + } + } + } + self.wasFilteredKeywordTested = true } open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift index 01a336ad60..7d56ccc0bf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -67,7 +67,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 3dc0a1b635..68872af019 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 16d3d6d133..d596377fd2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageShareButton/Sources/ChatMessageShareButton.swift @@ -103,7 +103,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 { @@ -147,6 +147,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 587acc94ba..f31b683d8d 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? @@ -418,7 +421,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 1e0dd952d7..3313b91a57 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -1040,7 +1040,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 70025dff94..3a4c483c1b 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 0d7f3737bf..89d523c732 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 2937054d2a..4b02633e6e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1044,7 +1044,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 b42b850c86..fc28977a59 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -169,6 +169,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol } public let openMessage: (Message, OpenMessageParams) -> Bool + // MARK: Swiftgram + public let sgStartMessageEdit: (Message) -> Void + public let sgGetChatPredictedLang: () -> String? public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void public let openPeerMention: (String, Promise?) -> Void public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void @@ -330,6 +333,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public init( openMessage: @escaping (Message, OpenMessageParams) -> Bool, + sgGetChatPredictedLang: @escaping () -> String? = { return nil }, + 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, @@ -447,6 +452,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol presentationContext: ChatPresentationContext ) { self.openMessage = openMessage + self.sgGetChatPredictedLang = sgGetChatPredictedLang + 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 cea4348d69..fcdec3bffa 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 @@ -469,7 +470,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 9160854d12..26366e6955 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 f6f46e22b6..4439c0f513 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -637,8 +637,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 fd0198c52d..cce0816dd8 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/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 9ad0c55a6e..8811150639 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -1087,7 +1087,6 @@ final class GiftOptionsScreenComponent: Component { let optionSpacing: CGFloat = 10.0 let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 - let showStarPrice = (self.starsState?.balance.value ?? 0) > 10 var hasGenericGifts = false @@ -1100,7 +1099,7 @@ final class GiftOptionsScreenComponent: Component { } let hasAnyGifts = hasGenericGifts || hasTransferGifts - if isSelfGift || isChannelGift || isPremiumDisabled { + if isSelfGift || isChannelGift || isPremiumDisabled || { return true }() /* MARK: Swiftgram */ { contentHeight += 6.0 } else { if let premiumProducts = state.premiumProducts { diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD b/submodules/TelegramUI/Components/LegacyInstantVideoController/BUILD index 4ffab8aeb7..2bc571eaa7 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 b6b6b4ffdf..d58bc3c73d 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/LegacyMessageInputPanel/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD index c367df9585..b69622b093 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD @@ -1,5 +1,6 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + swift_library( name = "LegacyMessageInputPanel", module_name = "LegacyMessageInputPanel", diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 373483fe9b..141202093b 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 @@ -138,6 +139,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: @@ -163,6 +166,8 @@ public enum MediaQualityPreset: Int32 { return 3000 case .compressedVeryHigh: return 6600 + case .videoMessageHD: + return 2000 case .videoMessage: return 1000 case .profileLow: @@ -183,9 +188,9 @@ public enum MediaQualityPreset: Int32 { var audioBitrateKbps: Int { switch self { case .compressedVeryLow, .compressedLow: - return 32 + return 32 * 2 case .compressedMedium, .compressedHigh, .compressedVeryHigh, .videoMessage: - return 64 + return 64 * 5 default: return 0 } @@ -1885,7 +1890,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat var values = values var videoBitrate: Int = 3700 - var audioBitrate: Int = 64 + var audioBitrate: Int = 64 * 5 var audioNumberOfChannels = 2 if image { videoBitrate = 5000 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 814fcb4edd..07fc3f27c3 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 0b3e30a019..286e374679 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 @@ -187,7 +188,7 @@ public extension MediaEditorScreenImpl { 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 { @@ -226,7 +227,7 @@ public extension MediaEditorScreenImpl { 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/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index 1963f35d45..b27643f862 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -1,5 +1,11 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgDeps = [ + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGInputToolbar:SGInputToolbar" +] + + swift_library( name = "MessageInputPanelComponent", module_name = "MessageInputPanelComponent", @@ -9,7 +15,7 @@ swift_library( copts = [ "-warnings-as-errors", ], - deps = [ + deps = sgDeps + [ "//submodules/Display", "//submodules/ComponentFlow", "//submodules/AppBundle", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 52492030e1..1a18b06321 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1,3 +1,8 @@ +// MARK: Swiftgram +import class SwiftUI.UIHostingController +import SGSimpleSettings +import SGInputToolbar + import Foundation import UIKit import Display @@ -474,6 +479,9 @@ public final class MessageInputPanelComponent: Component { private let counter = ComponentView() private var header: ComponentView? + // MARK: Swiftgram + private var toolbarView: UIView? + private var disabledPlaceholder: ComponentView? private var textClippingView = UIView() private let textField = ComponentView() @@ -529,7 +537,7 @@ public final class MessageInputPanelComponent: Component { return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView } - override init(frame: CGRect) { + init(context: AccountContext, frame: CGRect) { self.fieldBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.fieldBackgroundTint = UIView() self.fieldBackgroundTint.backgroundColor = UIColor(white: 1.0, alpha: 0.1) @@ -576,6 +584,9 @@ public final class MessageInputPanelComponent: Component { self.state?.updated() } ) + + // MARK: Swiftgram + self.initToolbarIfNeeded(context: context) } required init?(coder: NSCoder) { @@ -754,6 +765,11 @@ public final class MessageInputPanelComponent: Component { if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel { return panelResult } + + // MARK: Swiftgram + if result == nil, let toolbarView = self.toolbarView, let toolbarResult = toolbarView.hitTest(self.convert(point, to: toolbarView), with: event) { + return toolbarResult + } return result } @@ -2467,12 +2483,15 @@ public final class MessageInputPanelComponent: Component { } } + // MARK: Swiftgram + size = self.layoutToolbar(transition: transition, layoutFromTop: layoutFromTop, size: size, availableSize: availableSize, defaultInsets: defaultInsets, textFieldSize: textFieldSize, previousComponent: previousComponent) + return size } } public func makeView() -> View { - return View(frame: CGRect()) + return View(context: self.context, frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -2520,3 +2539,109 @@ final class ViewForOverlayContent: UIView { return nil } } + + +extension MessageInputPanelComponent.View { + func initToolbarIfNeeded(context: AccountContext) { + guard #available(iOS 13.0, *) else { return } + guard SGSimpleSettings.shared.inputToolbar else { return } + guard context.sharedContext.immediateSGStatus.status > 1 else { return } + guard self.toolbarView == nil else { return } + let notificationName = Notification.Name("sgToolbarAction") + let toolbar = ChatToolbarView( + onQuote: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "quote"]) + }, + onSpoiler: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "spoiler"]) + }, + onBold: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "bold"]) + }, + onItalic: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "italic"]) + }, + onMonospace: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "monospace"]) + }, + onLink: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "link"]) + }, + onStrikethrough: { [weak self] + in guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "strikethrough"]) + }, + onUnderline: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "underline"]) + }, + onCode: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "code"]) + }, + onNewLine: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "newline"]) + }, + // TODO(swiftgram): Binding + showNewLine: .constant(true), //.constant(self.sendWithReturnKey) + onClearFormatting: { [weak self] in + guard let _ = self else { return } + NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "clearFormatting"]) + } + ).colorScheme(.dark) + + let toolbarHostingController = UIHostingController(rootView: toolbar) + toolbarHostingController.view.backgroundColor = .clear + let toolbarView = toolbarHostingController.view + self.toolbarView = toolbarView + // assigning toolbarHostingController bugs responsivness and overrides layout + // self.toolbarHostingController = toolbarHostingController + + // Disable "Swipe to go back" gesture when touching scrollview + self.interactiveTransitionGestureRecognizerTest = { [weak self] point in + if let self, let _ = self.toolbarView?.hitTest(point, with: nil) { + return false + } + return true + } + if let toolbarView = self.toolbarView { + self.addSubview(toolbarView) + } + } + + func layoutToolbar(transition: ComponentTransition, layoutFromTop: Bool, size: CGSize, availableSize: CGSize, defaultInsets: UIEdgeInsets, textFieldSize: CGSize, previousComponent: MessageInputPanelComponent?) -> CGSize { + // TODO(swiftgram): Do not show if locked formatting + var transition = transition + if let previousComponent = previousComponent { + let previousLayoutFromTop = previousComponent.attachmentButtonMode == .captionDown + if previousLayoutFromTop != layoutFromTop { + // attachmentButtonMode changed + transition = .immediate + } + } + var size = size + if let toolbarView = self.toolbarView { + let toolbarHeight: CGFloat = 44.0 + let toolbarSpacing: CGFloat = 1.0 + let toolbarSize = CGSize(width: availableSize.width, height: toolbarHeight) + let hasFirstResponder = self.hasFirstResponder() + transition.setAlpha(view: toolbarView, alpha: hasFirstResponder ? 1.0 : 0.0) + if layoutFromTop { + transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: availableSize.height + toolbarSpacing), size: toolbarSize)) + } else { + transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: textFieldSize.height + defaultInsets.top + toolbarSpacing), size: toolbarSize)) + if hasFirstResponder { + size.height += toolbarHeight + toolbarSpacing + } + } + } + return size + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index a86f77797d..6ff9d984de 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 ab8742bd26..70fbd1000e 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 @@ -352,6 +354,8 @@ final class PeerInfoPersonalChannelData: Equatable { } final class PeerInfoScreenData { + let regDate: RegDate? + let channelCreationTimestamp: Int32? let peer: Peer? let chatPeer: Peer? let savedMessagesPeer: Peer? @@ -401,6 +405,8 @@ final class PeerInfoScreenData { } init( + regDate: RegDate? = nil, + channelCreationTimestamp: Int32? = nil, peer: Peer?, chatPeer: Peer?, savedMessagesPeer: Peer?, @@ -439,6 +445,8 @@ final class PeerInfoScreenData { premiumGiftOptions: [PremiumGiftCodeOption], webAppPermissions: WebAppPermissionsState? ) { + self.regDate = regDate + self.channelCreationTimestamp = channelCreationTimestamp self.peer = peer self.chatPeer = chatPeer self.savedMessagesPeer = savedMessagesPeer @@ -878,6 +886,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 } @@ -1311,6 +1323,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()), @@ -1332,7 +1345,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen premiumGiftOptions, webAppPermissions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in + |> map { regDate, peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1420,6 +1433,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return PeerInfoScreenData( + regDate: regDate, peer: peer, chatPeer: peerView.peers[peerId], savedMessagesPeer: savedMessagesPeer?._asPeer(), @@ -1565,6 +1579,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: peerId) 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()), @@ -1584,7 +1599,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen revenueContextAndState, profileGiftsContext.state ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState, profileGiftsState -> PeerInfoScreenData in + |> map { firstMessage, peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState, profileGiftsState -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -1642,6 +1657,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return PeerInfoScreenData( + channelCreationTimestamp: firstMessage?.timestamp, peer: peerView.peers[peerId], chatPeer: peerView.peers[peerId], savedMessagesPeer: nil, @@ -1879,6 +1895,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()), @@ -1898,7 +1915,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPremiumRequiredForStoryPosting, starsRevenueContextAndState ) - |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal in + |> mapToSignal { firstMessage, peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> 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 @@ -1967,7 +1984,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, @@ -2412,3 +2446,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 43a402e294..d69b1575bc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -122,7 +122,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? @@ -131,7 +131,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 a40ab2c8e6..a947105441 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButtonContainerNode.swift @@ -115,6 +115,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 { @@ -164,6 +172,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 af54baca78..3ba26c27ff 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? @@ -191,8 +193,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var validLayout: (width: CGFloat, statusBarHeight: 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 @@ -1157,8 +1160,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { if title.replacingOccurrences(of: "\u{fe0e}", with: "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { title = "" //"\u{00A0}" } + // 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)" @@ -1172,10 +1176,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 5a8a576743..cd945f1ad9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1,3 +1,9 @@ +// MARK: Swiftgram +import SGDebugUI +import SGSimpleSettings +import SGSettingsUI +import SGStrings +import CountrySelectionUI import Foundation import UIKit import Display @@ -327,10 +333,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 @@ -491,6 +497,8 @@ private enum PeerInfoMemberAction { } private enum PeerInfoContextSubject { + case copy(String) + case aboutDC case bio case phone(String) case link(customLink: String?) @@ -500,6 +508,8 @@ private enum PeerInfoContextSubject { } private enum PeerInfoSettingsSection { + case swiftgram + case swiftgramPro case avatar case edit case proxy @@ -547,6 +557,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 @@ -620,6 +631,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, @@ -692,6 +704,7 @@ private final class PeerInfoInteraction { displayAutoTranslateLocked: @escaping () -> Void, getController: @escaping () -> ViewController? ) { + self.notifyTextCopied = notifyTextCopied self.openUsername = openUsername self.openPhone = openPhone self.editingOpenNotificationSettings = editingOpenNotificationSettings @@ -765,9 +778,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 @@ -775,6 +788,7 @@ private enum SettingsSection: Int, CaseIterable { case accounts case myProfile case proxy + case swiftgram case apps case shortcuts case advanced @@ -783,7 +797,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 [] } @@ -835,6 +849,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)) @@ -899,10 +935,14 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) } - items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: { - interaction.openSettings(.addAccount) - })) +// items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: { +// interaction.openSettings(.addAccount) +// })) } + // MARK: Swiftgram + items[.accounts]!.append(PeerInfoScreenActionItem(id: 1000, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: { + interaction.openSettings(.addAccount) + })) items[.myProfile]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyProfile, icon: PresentationResourcesSettings.myProfile, action: { interaction.openSettings(.profile) @@ -926,6 +966,29 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } + // 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 + } + + + let sgWebSettings = context.currentAppConfiguration.with({ $0 }).sgWebSettings + if sgWebSettings.global.paymentsEnabled || context.sharedContext.immediateSGStatus.status > 1 { + items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: 0, label: .titleBadge(presentationData.strings.Settings_New, presentationData.theme.list.itemAccentColor), text: "Swiftgram Pro", icon: PresentationResourcesSettings.swiftgramPro, action: { + interaction.openSettings(.swiftgramPro) + })) + } + items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: 1, label: swiftgramLabel, text: "Swiftgram", icon: PresentationResourcesSettings.swiftgram, action: { + interaction.openSettings(.swiftgram) + })) + var appIndex = 1000 if let settings = data.globalSettings { for bot in settings.bots { @@ -1033,7 +1096,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p })) } if let starsState = data.starsState { - if !isPremiumDisabled || starsState.balance > StarsAmount.zero { + if (!isPremiumDisabled || starsState.balance > StarsAmount.zero) && sgWebSettings.global.canGrant { items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 104, label: .text(""), text: presentationData.strings.Settings_SendGift, icon: PresentationResourcesSettings.premiumGift, action: { interaction.openSettings(.premiumGift) })) @@ -1237,6 +1300,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat } private enum InfoSection: Int, CaseIterable { + case swiftgram case groupLocation case calls case personalChannel @@ -1249,12 +1313,19 @@ private enum InfoSection: Int, CaseIterable { case botAffiliateProgram } -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 isMutualContact = false + // var isUser = false + // let lang = presentationData.strings.baseLanguageCode var items: [InfoSection: [PeerInfoScreenItem]] = [:] for section in InfoSection.allCases { @@ -1278,6 +1349,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if let user = data.peer as? TelegramUser { + // MARK: Swiftgram + isMutualContact = user.flags.contains(.mutualContact) + idText = String(user.id.id._internalGetInt64Value()) +// isUser = true + if !callMessages.isEmpty { items[.calls]!.append(PeerInfoScreenCallListItem(id: 20, messages: callMessages)) } @@ -1682,6 +1758,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 @@ -1692,6 +1771,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let ItemMemberRequests = 8 let ItemBalance = 9 let ItemEdit = 10 + let ItemSGRecentActions = 11 if let _ = data.threadData { let mainUsername: String @@ -1928,10 +2008,21 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: settingsTitle, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { interaction.openEditing() })) + + // MARK: Swiftgram + if channel.hasPermission(.banMembers) || channel.flags.contains(.isCreator) { + items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemSGRecentActions, label: .none, text: presentationData.strings.Group_Info_AdminLog, icon: UIImage(bundleImageName: "Chat/Info/RecentActionsIcon"), action: { + interaction.openRecentActions() + })) + } + // } } } } 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 { @@ -2002,6 +2093,139 @@ 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 dcId: Int? = nil +// var dcLocation: String = "" + var phoneCountryText = "" + + var dcLabel = "" + var dcText: String = "" + + if let cachedData = data.cachedData as? CachedUserData, let phoneCountry = cachedData.peerStatusSettings?.phoneCountry { + var countryName = "" + let countriesConfiguration = context.currentCountriesConfiguration.with { $0 } + if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) { + countryName = country.localizedName ?? country.name + } else if phoneCountry == "FT" { + countryName = presentationData.strings.Chat_NonContactUser_AnonymousNumber + } else if phoneCountry == "TS" { + countryName = "Test" + } + phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName + } + if let peer = data.peer, let smallProfileImage = peer.smallProfileImage, let cloudResource = smallProfileImage.resource as? CloudPeerPhotoSizeMediaResource { + dcId = cloudResource.datacenterId + +// switch (dcId) { +// case 1: +// dcLocation = "Miami" +// case 2: +// dcLocation = "Amsterdam" +// case 3: +// dcLocation = "Miami" +// case 4: +// dcLocation = "Amsterdam" +// case 5: +// dcLocation = "Singapore" +// default: +// break +// } + } + + if let dcId = dcId { + dcLabel = "dc: \(dcId)" + if phoneCountryText.isEmpty { +// if !dcLocation.isEmpty { +// dcLabel += " \(dcLocation)" +// } + } else { + dcText = "\(phoneCountryText)" + } + } else if !phoneCountryText.isEmpty { + dcLabel = "dc: ?" + dcText = phoneCountryText + } + + if !dcText.isEmpty || !dcLabel.isEmpty { + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: dcLabel, text: dcText, 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 { + var regDateString = "" + if let cachedData = data.cachedData as? CachedUserData, let registrationDate = cachedData.peerStatusSettings?.registrationDate { + let components = registrationDate.components(separatedBy: ".") + if components.count == 2, let first = Int32(components[0]), let second = Int32(components[1]) { + let month = first - 1 + let year = second - 1900 + regDateString = stringForMonth(strings: presentationData.strings, month: month, ofYear: year) + } + } + if let regDate = data.regDate, regDateString.isEmpty { + let regTimestamp = Int32((regDate.from + regDate.to) / 2) + 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) + } + } + if !regDateString.isEmpty { + 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 + } + } + if isMutualContact { + items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("MutualContact.Label", presentationData.strings.baseLanguageCode), text: "", action: nil, longTapAction: { _ in }, requestLayout: { _ in + interaction.requestLayout(false) + })) + sgItemId += 1 + } + + var result: [(AnyHashable, [PeerInfoScreenItem])] = [] for section in InfoSection.allCases { if let sectionItems = items[section], !sectionItems.isEmpty { @@ -2812,7 +3036,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 @@ -2858,6 +3082,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? var state = PeerInfoState( isEditing: false, @@ -2935,7 +3161,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?, profileGiftsContext: ProfileGiftsContext?, 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?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, initialPaneKey: PeerInfoPaneKey?) { self.controller = controller self.context = context self.peerId = peerId @@ -2960,7 +3186,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() @@ -2968,6 +3194,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) }, @@ -4512,7 +4742,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 { @@ -4943,14 +5173,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 { @@ -6488,10 +6739,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { + if /* MARK: Swiftgram */ strongSelf.context.currentAppConfiguration.with({ $0 }).sgWebSettings.global.canGrant && strongSelf.peerId.namespace == Namespaces.Peer.CloudUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { if let cachedData = data.cachedData as? CachedUserData, cachedData.disallowedGifts == .All { } else { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, icon: { theme in + items.append(.action(ContextMenuActionItem(text: "Telegram Gifts", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -6651,8 +6902,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } else if let channel = peer as? TelegramChannel { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { - if case .broadcast = channel.info, cachedData.flags.contains(.starGiftsAvailable) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, badge: nil, icon: { theme in + if case .broadcast = channel.info, cachedData.flags.contains(.starGiftsAvailable), strongSelf.context.currentAppConfiguration.with({ $0 }).sgWebSettings.global.canGrant { + items.append(.action(ContextMenuActionItem(text: "Telegram Gifts", badge: nil, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -6734,6 +6985,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) }))) } + // MARK: Swiftgram + if case .group = channel.info { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_Administrators, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat List/ProxyShieldIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, f in + f(.dismissWithoutContent) + self?.openParticipantsSection(section: .admins) + }))) + } if canSetupAutoremoveTimeout { let strings = strongSelf.presentationData.strings @@ -7712,7 +7972,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 { @@ -9555,6 +9815,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 { @@ -9583,7 +9879,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) @@ -10273,6 +10569,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 @@ -10288,6 +10585,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } switch section { + case .swiftgram: + self.controller?.push(sgSettingsController(context: self.context)) + case .swiftgramPro: + if self.context.sharedContext.immediateSGStatus.status > 1 { + self.controller?.push(self.context.sharedContext.makeSGProController(context: self.context)) + } else { + if let payWallController = self.context.sharedContext.makeSGPayWallController(context: self.context) { + self.controller?.present(payWallController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + self.controller?.present(self.context.sharedContext.makeSGUpdateIOSController(), animated: true) + } + } case .avatar: self.controller?.openAvatarForEditing() case .edit: @@ -10462,15 +10771,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 } @@ -10490,7 +10799,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: @@ -11132,7 +11451,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) @@ -11874,7 +12193,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) { @@ -12285,6 +12604,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 { @@ -12748,6 +13068,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc private var tabBarItemDisposable: Disposable? + private let hidePhoneInSettings: Bool + var controllerNode: PeerInfoScreenNode { return self.displayNode as! PeerInfoScreenNode } @@ -12786,8 +13108,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc var didAppear: Bool = false private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - + public init( + hidePhoneInSettings: Bool = SGSimpleSettings.defaultValues[SGSimpleSettings.Keys.hidePhoneInSettings.rawValue] as! Bool, context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, @@ -12807,6 +13130,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc switchToGroupsInCommon: Bool = false ) { self.context = context + self.hidePhoneInSettings = hidePhoneInSettings self.updatedPresentationData = updatedPresentationData self.peerId = peerId self.avatarInitiallyExpanded = avatarInitiallyExpanded @@ -13176,7 +13500,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } else if self.switchToGroupsInCommon { initialPaneKey = .groupsInCommon } - 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, profileGiftsContext: self.profileGiftsContext, 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, profileGiftsContext: self.profileGiftsContext, 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()) @@ -13440,6 +13764,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 @@ -14590,3 +14930,90 @@ private func cancelContextGestures(view: UIView) { cancelContextGestures(view: subview) } } + + +// 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/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 8fe02c1c2b..d8303c4ec9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -776,6 +776,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.panelButton = panelButton panelButton.title = self.peerId == self.context.account.peerId ? params.presentationData.strings.PeerInfo_Gifts_Send : params.presentationData.strings.PeerInfo_Gifts_SendGift + panelButton.title = "Telegram Gifts" panelButton.pressed = { [weak self] in self?.buttonPressed() diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 3bb37cc827..7f99be0b9b 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -13,6 +13,7 @@ import CounterControllerTitleView public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController { private let context: AccountContext + private let forceHideNames: Bool private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -94,6 +95,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon public init(_ params: PeerSelectionControllerParams) { self.context = params.context + self.forceHideNames = params.forceHideNames self.filter = params.filter self.forumPeerId = params.forumPeerId self.hasFilters = params.hasFilters @@ -252,7 +254,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon } override public func loadDisplayNode() { - self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, present: { [weak self] c, a in + self.displayNode = PeerSelectionControllerNode(context: self.context, forceHideNames: self.forceHideNames, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasFilters: self.hasFilters, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, hasCreation: self.hasCreation, createNewGroup: self.createNewGroup, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 3e311386e4..24498672a3 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -109,7 +109,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (self.presentationData, self.presentationDataPromise.get()) } - init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, forceHideNames: Bool = false, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.controller = controller self.present = present @@ -130,6 +130,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } + // MARK: Swiftgram + if forceHideNames { + self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardOptionsState(ChatInterfaceForwardOptionsState(hideNames: true, hideCaptions: false, unhideNamesOnCaptionChange: false)) + } + } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) if let _ = self.requestPeerType { @@ -353,9 +358,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))) }) }) @@ -461,7 +466,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, action: { [weak self] _, f in self?.interfaceInteraction?.updateForwardOptionsState({ current in var updated = current - updated.hideNames = false + updated.hideNames = false || forceHideNames updated.hideCaptions = false updated.unhideNamesOnCaptionChange = false return updated diff --git a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift index e02e775051..de886635ba 100644 --- a/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Components/Resources/FetchVideoMediaResource/Sources/FetchVideoMediaResource.swift @@ -847,6 +847,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 c5f800167d..658a63df3a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -651,7 +651,7 @@ final class StarsTransactionsScreenComponent: Component { count: self.starsState?.balance ?? StarsAmount.zero, rate: nil, actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy, - actionAvailable: !premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled, + actionAvailable: false, /* MARK: Swiftgram */ // !premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled, actionIsEnabled: true, actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), action: { [weak self] in diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageKeepSizeComponent.swift index f59150b4d1..86445a2926 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 6994ba3d69..b6847d0061 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -1949,7 +1949,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 @@ -3197,19 +3198,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 540e1b1bc4..23c35fd404 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 0000000000..0121039caf --- /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 f1629ae208..4b68963f69 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() } @@ -1308,6 +1318,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 } @@ -1365,6 +1397,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) @@ -1486,7 +1523,7 @@ private final class StoryContainerScreenComponent: Component { if self.pendingNavigationToItemId != nil { isProgressPaused = true } - if self.isDisplayingInteractionGuide { + if self.isDisplayingInteractionGuide || self.isDisplayingStoriesWarning { isProgressPaused = true } @@ -1914,6 +1951,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 856df6d79b..964d0f1d5d 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/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 8fa75ffef1..78680d7cf0 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -311,6 +311,9 @@ public final class TextFieldComponent: Component { return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange) } + // MARK: Swiftgram + var sgToolbarActionObserver: NSObjectProtocol? = nil + private var component: TextFieldComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false @@ -389,6 +392,21 @@ public final class TextFieldComponent: Component { ) } } + + // MARK: Swiftgram + self.sgToolbarActionObserver = NotificationCenter.default.addObserver(forName: Notification.Name("sgToolbarAction"), object: nil, queue: .main, using: { [weak self] notification in + guard let self = self else { return } + if let action = notification.userInfo?["action"] as? String { + self.sgToolbarAction(action) + } + }) + } + + // MARK: Swiftgram + deinit { + if let sgToolbarActionObserver = self.sgToolbarActionObserver { + NotificationCenter.default.removeObserver(sgToolbarActionObserver) + } } required init?(coder: NSCoder) { @@ -1755,3 +1773,134 @@ extension TextFieldComponent.InputState { } } } + + +extension TextFieldComponent.View { + + func sgToolbarAction(_ action: String) { + switch action { + case "quote": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)) + case "spoiler": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.spoiler) + case "bold": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.bold) + case "italic": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.italic) + case "monospace": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.monospace) + case "link": + self.sgSelectLastWordIfIdle() + self.openLinkEditing() + case "strikethrough": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.strikethrough) + case "underline": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.underline) + case "code": + self.sgSelectLastWordIfIdle() + self.toggleAttribute(key: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)) + case "newline": + self.sgSetNewLine() + case "clearFormatting": + self.updateInputState { current in + return current.clearFormattingAttributes() + } + default: + assert(false, "Unhandled action \(action)") + } + } + + func sgSelectLastWordIfIdle() { + self.updateInputState { current in + // No changes to current selection + if !current.selectionRange.isEmpty { + return current + } + + let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString() + + // If text is empty or cursor is at the start, return current state + guard inputText.length > 0, current.selectionRange.lowerBound > 0 else { + return current + } + + let plainText = inputText.string + let nsString = plainText as NSString + + // Create character set for word boundaries + let wordBoundaries = CharacterSet.whitespacesAndNewlines + + // Start from cursor position instead of end of text + var endIndex = current.selectionRange.lowerBound - 1 + + // Find last non-whitespace character before cursor + while endIndex >= 0 && + (nsString.substring(with: NSRange(location: endIndex, length: 1)) as NSString) + .rangeOfCharacter(from: wordBoundaries).location != NSNotFound { + endIndex -= 1 + } + + // If we only had whitespace before cursor, return current state + guard endIndex >= 0 else { + return current + } + + // Find start of the current word by looking backwards for whitespace + var startIndex = endIndex + while startIndex > 0 { + let char = nsString.substring(with: NSRange(location: startIndex - 1, length: 1)) + if (char as NSString).rangeOfCharacter(from: wordBoundaries).location != NSNotFound { + break + } + startIndex -= 1 + } + + // Create range for the word at cursor + let wordLength = endIndex - startIndex + 1 + let wordRange = NSRange(location: startIndex, length: wordLength) + + // Create new selection range + let newSelectionRange = wordRange.location ..< (wordRange.location + wordLength) + + return TextFieldComponent.InputState(inputText: inputText, selectionRange: newSelectionRange) + } + } + + func sgSetNewLine() { + self.updateInputState { current in + let inputText = (current.inputText.mutableCopy() as? NSMutableAttributedString) ?? NSMutableAttributedString() + + // Check if there's selected text + let hasSelection = current.selectionRange.count > 0 + + if hasSelection { + // Move selected text to new line + let selectedText = inputText.attributedSubstring(from: NSRange(current.selectionRange)) + let newLineAttr = NSAttributedString(string: "\n") + + // Insert newline and selected text + inputText.replaceCharacters(in: NSRange(current.selectionRange), with: newLineAttr) + inputText.insert(selectedText, at: current.selectionRange.lowerBound + 1) + + // Update selection range to end of moved text + let newPosition = current.selectionRange.lowerBound + 1 + selectedText.length + return TextFieldComponent.InputState(inputText: inputText, selectionRange: newPosition ..< newPosition) + } else { + // Simple newline insertion at current position + let attributedString = NSAttributedString(string: "\n") + inputText.replaceCharacters(in: NSRange(current.selectionRange), with: attributedString) + + // Update cursor position + let newPosition = current.selectionRange.lowerBound + attributedString.length + return TextFieldComponent.InputState(inputText: inputText, selectionRange: newPosition ..< newPosition) + } + } + } +} diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD b/submodules/TelegramUI/Components/VideoMessageCameraScreen/BUILD index d39746aa69..0df18ec298 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 7101eab38d..5f2f436584 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 937fc44540..1cc6ddeffd 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 0e2514e3b1..012df8f398 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 @@ -785,6 +788,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 } @@ -852,6 +857,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 a28067e1b4..19a6d0e862 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1,3 +1,15 @@ +// MARK: Swiftgram +import StoreKit +import SGIAP +import SGAPI +import SGDeviceToken +import SGAPIToken + +import SGActionRequestHandlerSanitizer +import SGAPIWebSettings +import SGLogging +import SGStrings +import SGSimpleSettings import UIKit import SwiftSignalKit import Display @@ -235,7 +247,7 @@ private func extractAccountManagerState(records: AccountRecordsView(false) private let sharedContextPromise = Promise() - //private let watchCommunicationManagerPromise = Promise() + private let watchCommunicationManagerPromise = Promise() private var accountManager: AccountManager? private var accountManagerState: AccountManagerState? @@ -593,6 +605,12 @@ private func extractAccountManagerState(records: AccountRecordsView mapToSignal { context -> Signal in if let context = context, let watchManager = context.context.watchManager { let accountId = context.context.account.id @@ -1079,7 +1122,7 @@ private func extractAccountManagerState(records: AccountRecordsView map { ($0.0?.account, $0.1.map { ($0.0, $0.1.account) }) }, liveLocationPolling: liveLocationPolling, watchTasks: .single(nil), inForeground: applicationBindings.applicationInForeground, hasActiveAudioSession: self.hasActiveAudioSession.get(), notificationManager: notificationManager, mediaManager: sharedContext.mediaManager, callManager: sharedContext.callManager, accountUserInterfaceInUse: { id in + }, activeAccounts: sharedContext.activeAccountContexts |> map { ($0.0?.account, $0.1.map { ($0.0, $0.1.account) }) }, liveLocationPolling: liveLocationPolling, watchTasks: watchTasks /* MARK: Swiftgram */, inForeground: applicationBindings.applicationInForeground, hasActiveAudioSession: self.hasActiveAudioSession.get(), notificationManager: notificationManager, mediaManager: sharedContext.mediaManager, callManager: sharedContext.callManager, accountUserInterfaceInUse: { id in return sharedContext.accountUserInterfaceInUse(id) }) let sharedApplicationContext = SharedApplicationContext(sharedContext: sharedContext, notificationManager: notificationManager, wakeupManager: wakeupManager) @@ -1110,7 +1153,7 @@ private func extractAccountManagerState(records: AccountRecordsView() + let watchManagerArgumentsPromise = Promise() self.context.set(self.sharedContextPromise.get() |> deliverOnMainQueue @@ -1149,7 +1192,7 @@ 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: watchManagerArgumentsPromise.get(), 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 @@ -1226,6 +1269,8 @@ private func extractAccountManagerState(records: AccountRecordsView), NoError> = self.sharedContextPromise.get() @@ -1393,7 +1458,7 @@ private func extractAccountManagerState(records: AccountRecordsView flatMap { WatchCommunicationManagerContext(context: $0.context) }, allowBackgroundTimeExtension: { timeout in + self.watchCommunicationManagerPromise.set(watchCommunicationManager(context: self.context.get() |> flatMap { WatchCommunicationManagerContext(context: $0.context) }, allowBackgroundTimeExtension: { timeout in let _ = (self.sharedContextPromise.get() |> take(1)).start(next: { sharedContext in sharedContext.wakeupManager.allowBackgroundTimeExtension(timeout: timeout) @@ -1405,7 +1470,7 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue).start(next: { sharedApplicationContext in @@ -1960,6 +2026,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() } }) @@ -2475,6 +2546,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 { @@ -3237,3 +3309,171 @@ private func getMemoryConsumption() -> Int { } return Int(info.phys_footprint) } + +// MARK: Swiftgram +@available(iOS 13.0, *) +extension AppDelegate { + + func setupIAP() { + NotificationCenter.default.addObserver(forName: .SGIAPHelperPurchaseNotification, object: nil, queue: nil) { [weak self] notification in + SGLogger.shared.log("SGIAP", "Got SGIAPHelperPurchaseNotification") + guard let strongSelf = self else { return } + if let transactions = notification.object as? [SKPaymentTransaction] { + let _ = (strongSelf.context.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak strongSelf] context in + guard let veryStrongSelf = strongSelf else { + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } + return + } + guard let context = context else { + SGLogger.shared.log("SGIAP", "Empty app context (how?)") + + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } + return + } + SGLogger.shared.log("SGIAP", "Got context for SGIAPHelperPurchaseNotification") + let _ = Task { + await veryStrongSelf.sendReceiptForVerification(primaryContext: context.context) + await veryStrongSelf.fetchSGStatus(primaryContext: context.context) + + SGLogger.shared.log("SGIAP", "Finishing transactions \(transactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))") + let defaultPaymentQueue = SKPaymentQueue.default() + for transaction in transactions { + defaultPaymentQueue.finishTransaction(transaction) + } + } + }) + } else { + SGLogger.shared.log("SGIAP", "Wrong object in SGIAPHelperPurchaseNotification") + #if DEBUG + preconditionFailure("Wrong object in SGIAPHelperPurchaseNotification") + #endif + } + } + } + + func getPrimaryContext(anyContext context: AccountContext, fallbackToCurrent: Bool = false) async -> AccountContext { + var primaryUserId: Int64 = Int64(SGSimpleSettings.shared.primaryUserId) ?? 0 + if primaryUserId == 0 { + primaryUserId = context.account.peerId.id._internalGetInt64Value() + } + + var primaryContext = try? await getContextForUserId(context: context, userId: primaryUserId).awaitable() + if let primaryContext = primaryContext { + SGLogger.shared.log("SGIAP", "Got primary context for user id: \(primaryContext.account.peerId.id._internalGetInt64Value())") + return primaryContext + } else { + primaryContext = context + let newPrimaryUserId = context.account.peerId.id._internalGetInt64Value() + SGLogger.shared.log("SGIAP", "Primary context for user id \(primaryUserId) is nil! Falling back to current context with user id: \(newPrimaryUserId)") + return context + } + } + + func sendReceiptForVerification(primaryContext: AccountContext) async { + guard let receiptData = getPurchaceReceiptData() else { + return + } + + let encodedReceiptData = receiptData.base64EncodedData(options: []) + + var deviceToken: String? + var apiToken: String? + do { + async let deviceTokenTask = getDeviceToken().awaitable() + async let apiTokenTask = getSGApiToken(context: primaryContext).awaitable() + + (deviceToken, apiToken) = try await (deviceTokenTask, apiTokenTask) + } catch { + SGLogger.shared.log("SGIAP", "Error getting device token or API token: \(error)") + return + } + + if let deviceToken, let apiToken { + do { + let _ = try await postSGReceipt(token: apiToken, + deviceToken: deviceToken, + encodedReceiptData: encodedReceiptData).awaitable() + } catch let error as SignalCompleted { + let _ = error + } catch { + SGLogger.shared.log("SGIAP", "Error: \(error)") + } + } + } + + func fetchSGStatus(primaryContext: AccountContext) async { + // TODO(swiftgram): Stuck on getting shouldKeepConnection + // Perhaps, we can drop on some timeout? +// let currentShouldKeepConnection = await (primaryContext.account.network.shouldKeepConnection.get() |> take(1) |> deliverOnMainQueue).awaitable() + guard !primaryContext.account.testingEnvironment else { + return + } + let currentShouldKeepConnection = false + let userId = primaryContext.account.peerId.id._internalGetInt64Value() +// SGLogger.shared.log("SGIAP", "User id \(userId) currently keeps connection: \(currentShouldKeepConnection)") + if !currentShouldKeepConnection { + SGLogger.shared.log("SGIAP", "Asking user id \(userId) to keep connection: true") + primaryContext.account.network.shouldKeepConnection.set(.single(true)) + } + let iqtpResponse = try? await sgIqtpQuery(engine: primaryContext.engine, query: makeIqtpQuery(0, "s")).awaitable() + guard let iqtpResponse = iqtpResponse else { + SGLogger.shared.log("SGIAP", "IQTP response is nil!") +// if !currentShouldKeepConnection { +// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false") +// primaryContext.account.network.shouldKeepConnection.set(.single(false)) +// } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .SGIAPHelperValidationErrorNotification, object: nil, userInfo: ["error": "PayWall.ValidationError.TryAgain"]) + } + return + } + SGLogger.shared.log("SGIAP", "Got IQTP response: \(iqtpResponse)") + let _ = try? await updateSGStatusInteractively(accountManager: primaryContext.sharedContext.accountManager, { value in + var value = value + + let newStatus: Int64 + if let description = iqtpResponse.description, let status = Int64(description) { + newStatus = status + } else { + SGLogger.shared.log("SGIAP", "Can't parse IQTP response into status!") + newStatus = value.status // unparseable + } + + let userId = primaryContext.account.peerId.id._internalGetInt64Value() + if value.status != newStatus { + SGLogger.shared.log("SGIAP", "Updating \(userId) status \(value.status) -> \(newStatus)") + if newStatus > 1 { + let stringUserId = String(userId) + if SGSimpleSettings.shared.primaryUserId != stringUserId { + SGLogger.shared.log("SGIAP", "Setting new primary user id: \(userId)") + SGSimpleSettings.shared.primaryUserId = stringUserId + } + } + value.status = newStatus + } else { + SGLogger.shared.log("SGIAP", "Status \(value.status) for \(userId) hasn't changed") + if newStatus < 1 { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .SGIAPHelperValidationErrorNotification, object: nil, userInfo: ["error": "PayWall.ValidationError.Expired"]) + } + } + } + return value + }).awaitable() + +// if !currentShouldKeepConnection { +// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false") +// primaryContext.account.network.shouldKeepConnection.set(.single(false)) +// } + } +} diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index f27d52dbb9..cedf1504b0 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] @@ -782,18 +787,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 010ccc7f6e..8afb1cfea2 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -693,7 +693,7 @@ extension ChatControllerImpl { if counterAndTimestamp.0 >= 3 { maybeSuggestPremium = true } - if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden { + if (isPremium || maybeSuggestPremium || hasAutoTranslate || true /* MARK: Swiftgram */) && !isHidden { return chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId) |> map { translationState -> ChatPresentationTranslationState? in if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) { @@ -714,6 +714,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 + } + }) } let premiumGiftOptions: Signal<[CachedPremiumGiftOption], NoError> = .single([]) @@ -2223,12 +2239,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 @@ -2238,7 +2266,7 @@ 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 { guard !strongSelf.presentAccountFrozenInfoIfNeeded(delay: true) else { return @@ -2246,7 +2274,22 @@ extension ChatControllerImpl { 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 { @@ -5148,4 +5191,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 455ea29bdf..398ae6129c 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 537d9c2d61..9475b51717 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 aa4889525c..2a575e2076 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 e6c1fbc74d..194c64cd91 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 @@ -591,6 +592,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var translationStateDisposable: Disposable? + var chatLanguagePredictionDisposable: Disposable? + var predictedChatLanguage: String? var premiumGiftSuggestionDisposable: Disposable? var nextChannelToReadDisposable: Disposable? @@ -599,7 +602,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 @@ -1537,6 +1540,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.controllerInteraction?.isOpeningMediaSignal = openChatMessageParams.blockInteraction.get() return context.sharedContext.openChatMessage(openChatMessageParams) + }, sgGetChatPredictedLang: { [weak self] in + if let strongSelf = self { + var result: String? + if let chatPeerId = strongSelf.chatLocation.peerId { + result = SGSimpleSettings.shared.outgoingLanguageTranslation[SGSimpleSettings.makeOutgoingLanguageTranslationKey(accountId: strongSelf.context.account.peerId.id._internalGetInt64Value(), peerId: chatPeerId.id._internalGetInt64Value())] + } + return result ?? strongSelf.predictedChatLanguage + } + return nil + }, 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 { @@ -6134,7 +6150,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(), @@ -6172,7 +6188,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(), @@ -6786,7 +6802,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) @@ -7477,6 +7493,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 b856b26bbc..986ae1b396 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -16,7 +16,7 @@ import TopMessageReactions import ChatMessagePaymentAlertController 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) )) @@ -24,11 +24,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, .excludeDisabled, .doNotSearchMessages] var hasPublicPolls = false @@ -49,7 +49,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, forceHideNames: forceHideNames, 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 @@ -95,248 +95,190 @@ extension ChatControllerImpl { } } controller.multiplePeersSelected = { [weak self, weak controller] peers, peerMap, messageText, mode, forwardOptions, _ in - let peerIds = peers.map { $0.id } + guard let strongSelf = self, let strongController = controller else { + return + } + strongController.dismiss() - let _ = (context.engine.data.get( - EngineDataMap( - peerIds.map(TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.init(id:)) - ) - ) - |> deliverOnMainQueue).start(next: { [weak self, weak controller] sendPaidMessageStars in + var result: [EnqueueMessage] = [] + if messageText.string.count > 0 { + let inputText = convertMarkdownToAttributes(messageText) + for text in breakChatInputText(trimChatInputText(inputText)) { + if text.length != 0 { + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + } + } + } + + var attributes: [MessageAttribute] = [] + attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true, 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) + }) + + let commit: ([EnqueueMessage]) -> Void = { result in guard let strongSelf = self else { return } - var count: Int32 = Int32(messages.count) - if messageText.string.count > 0 { - count += 1 + var result = result + + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) + + var correlationIds: [Int64] = [] + for i in 0 ..< result.count { + let correlationId = Int64.random(in: Int64.min ... Int64.max) + correlationIds.append(correlationId) + result[i] = result[i].withUpdatedCorrelationId(correlationId) } - var totalAmount: StarsAmount = .zero - var chargingPeers: [EnginePeer] = [] - for peer in peers { - if let maybeAmount = sendPaidMessageStars[peer.id], let amount = maybeAmount { - totalAmount = totalAmount + amount - chargingPeers.append(peer) + + let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in + return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) + |> map { shouldDivert -> (EnginePeer, Bool) in + return (peer, shouldDivert) } } - - let proceed = { [weak self, weak controller] in - guard let strongSelf = self, let strongController = controller else { + let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) + let _ = (targetPeersShouldDivert + |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in + guard let strongSelf = self else { return } - strongController.dismiss() + var displayConvertingTooltip = false - var result: [EnqueueMessage] = [] - if messageText.string.count > 0 { - let inputText = convertMarkdownToAttributes(messageText) - for text in breakChatInputText(trimChatInputText(inputText)) { - if text.length != 0 { - var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) - if !entities.isEmpty { - attributes.append(TextEntitiesMessageAttribute(entities: entities)) + var displayPeers: [EnginePeer] = [] + for (peer, shouldDivert) in targetPeersShouldDivert { + var peerMessages = result + if shouldDivert { + displayConvertingTooltip = true + peerMessages = peerMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes } - result.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } } + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + 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()) + } + }) + + if case let .secretChat(secretPeer) = peer { + if let peer = peerMap[secretPeer.regularPeerId] { + displayPeers.append(peer) + } + } else { + displayPeers.append(peer) + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if displayPeers.count == 1, let peer = displayPeers.first { + var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string + } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { + var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string + } else if let peer = displayPeers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string + } else { + text = "" + } } - var attributes: [MessageAttribute] = [] - attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions?.hideNames == true, hideCaptions: forwardOptions?.hideCaptions == true)) + let reactionItems: Signal<[ReactionItem], NoError> + if savedMessages && messages.count > 0 { + reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) + } else { + reactionItems = .single([]) + } - result.append(contentsOf: messages.map { message -> EnqueueMessage in - return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) - }) - - let commit: ([EnqueueMessage]) -> Void = { result in - guard let strongSelf = self else { + let _ = (reactionItems + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in + guard let strongSelf else { return } - var result = result - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) - - var correlationIds: [Int64] = [] - for i in 0 ..< result.count { - let correlationId = Int64.random(in: Int64.min ... Int64.max) - correlationIds.append(correlationId) - result[i] = result[i].withUpdatedCorrelationId(correlationId) - } - - let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in - return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) - |> map { shouldDivert -> (EnginePeer, Bool) in - return (peer, shouldDivert) - } - } - let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) - let _ = (targetPeersShouldDivert - |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in - guard let strongSelf = self else { - return - } - - var displayConvertingTooltip = false - - var displayPeers: [EnginePeer] = [] - for (peer, shouldDivert) in targetPeersShouldDivert { - var peerMessages = result - if shouldDivert { - displayConvertingTooltip = true - peerMessages = peerMessages.map { message -> EnqueueMessage in - return message.withUpdatedAttributes { attributes in - var attributes = attributes - attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) - attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) - return attributes - } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return } - } - - if let maybeAmount = sendPaidMessageStars[peer.id], let amount = maybeAmount { - peerMessages = peerMessages.map { message -> EnqueueMessage in - return message.withUpdatedAttributes { attributes in - var attributes = attributes - attributes.append(PaidStarsMessageAttribute(stars: amount, postponeSending: false)) - return attributes - } - } - } - - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) - |> deliverOnMainQueue).startStandalone(next: { messageIds in - 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()) + guard let navigationController = self.navigationController as? NavigationController else { + return } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) }) - - if case let .secretChat(secretPeer) = peer { - if let peer = peerMap[secretPeer.regularPeerId] { - displayPeers.append(peer) - } - } else { - displayPeers.append(peer) - } } - - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many - savedMessages = true - } else { - if displayPeers.count == 1, let peer = displayPeers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string - } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string - } else if let peer = displayPeers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string - } else { - text = "" - } - } - - let reactionItems: Signal<[ReactionItem], NoError> - if savedMessages && messages.count > 0 { - reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) - } else { - reactionItems = .single([]) - } - - let _ = (reactionItems - |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in - guard let strongSelf else { - return - } - - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) - } - return false - }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) - }) - - if displayConvertingTooltip { - } - }) - } + return false + }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + }) - switch mode { - case .generic: - commit(result) - case .silent: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) - commit(transformedMessages) - case .schedule: - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in - if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime) - commit(transformedMessages) - } - }) - case .whenOnline: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) + if displayConvertingTooltip { + } + }) + } + + switch mode { + case .generic: + commit(result) + case .silent: + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) + commit(transformedMessages) + case .schedule: + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime) commit(transformedMessages) } - } - - if totalAmount.value > 0 { - let controller = chatMessagePaymentAlertController( - context: nil, - presentationData: strongSelf.presentationData, - updatedPresentationData: nil, - peers: chargingPeers, - count: count, - amount: totalAmount, - totalAmount: totalAmount, - hasCheck: false, - navigationController: strongSelf.navigationController as? NavigationController, - completion: { _ in - proceed() - } - ) - strongSelf.present(controller, in: .window(.root)) - } else { - proceed() - } - }) + }) + case .whenOnline: + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) + commit(transformedMessages) + } } controller.peerSelected = { [weak self, weak controller] peer, threadId in guard let strongSelf = self, let strongController = controller else { @@ -363,7 +305,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() @@ -383,7 +325,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 @@ -456,7 +398,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 34c5c34c27..a176dc0586 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 @@ -1597,8 +1598,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 57522caf86..06ce42cd63 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 @@ -342,6 +343,8 @@ private final class ChatHistoryTransactionOpaqueState { } private func extractAssociatedData( + translateToLanguageSG: String?, + translationSettings: TranslationSettings, chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, @@ -422,7 +425,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) + return ChatMessageItemAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, /* MARK: Swiftgram */ 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) } private extension ChatHistoryLocationInput { @@ -772,6 +775,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 @@ -942,7 +946,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } self.translationProcessingManager.process = { [weak self, weak context] messageIds in if let context = context, let translationLang = self?.translationLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone() + let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang, viaText: !context.isPremium).startStandalone() } } self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in @@ -1726,6 +1730,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var measure_isFirstTime = true let messageViewQueue = Queue.mainQueue() let historyViewTransitionDisposable = (combineLatest(queue: messageViewQueue, + self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1), historyViewUpdate |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_historyViewUpdate"), self.chatPresentationDataPromise.get() |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_chatPresentationData"), selectedMessages |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_selectedMessages"), @@ -1751,7 +1756,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto chatThemes |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_chatThemes"), deviceContactsNumbers |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_deviceContactsNumbers"), contentSettings |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_contentSettings") - ) |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_firstChatHistoryTransition")).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 in + ) |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_firstChatHistoryTransition")).startStrict(next: { [weak self] sharedData, /* MARK: Swiftgram */ 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 in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises if measure_isFirstTime { @@ -1762,6 +1767,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto #endif } + 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 { @@ -1964,18 +1976,26 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto displayForNotConsumed: suggestAudioTranscription.1, providedByGroupBoost: audioTranscriptionProvidedByBoost ) - - var translateToLanguage: (fromLang: String, toLang: String)? - if let translationState, (isPremium || autoTranslate) && translationState.isEnabled { + + // MARK: Swiftgram + // var translateToLanguage: (fromLang: String, toLang: String)? + // if let translationState, (isPremium || autoTranslate) && translationState.isEnabled { var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode let rawSuffix = "-raw" if languageCode.hasSuffix(rawSuffix) { languageCode = String(languageCode.dropLast(rawSuffix.count)) } + languageCode = normalizeTranslationLanguage(languageCode) + let translateToLanguageSG = languageCode + // } + var translateToLanguage: (fromLang: String, toLang: String)? + if let translationState, (isPremium || autoTranslate || true) && translationState.isEnabled { translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(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?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive")) + + + let associatedData = extractAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, /* MARK: Swiftgram */ 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")) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { @@ -2076,7 +2096,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var scrollAnimationCurve: ListViewAnimationCurve? = nil if let strongSelf = self, case .default = source { if let translateToLanguage { - strongSelf.translationLang = (fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang) + strongSelf.translationLang = (fromLang: nil, toLang: translateToLanguage) } else { strongSelf.translationLang = nil } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 0488f21816..4a7542a37c 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 @@ -258,7 +259,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 a1650a467d..46e059c0a8 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 @@ -485,6 +488,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 @@ -633,7 +646,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()) ?? "" } } @@ -927,6 +940,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let isPremium = accountPeer?.isPremium ?? false var actions: [ContextMenuItem] = [] + var sgActions: [ContextMenuItem] = [] var isPinnedMessages = false if case .pinnedMessages = chatPresentationInterfaceState.subject { @@ -1161,6 +1175,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) }) }))) + if !SGSimpleSettings.shared.contextShowReply { sgActions.append(actions.removeLast()) } } if data.messageActions.options.contains(.sendScheduledNow) { @@ -1268,7 +1283,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()) ?? "" } } @@ -1385,6 +1400,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }) f(.default) }))) + if !SGSimpleSettings.shared.contextShowSaveMedia { sgActions.append(actions.removeLast()) } } } @@ -1430,6 +1446,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 { @@ -1468,6 +1496,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controllerInteraction.openMessageReplies(messages[0].id, true, true) }) }))) + if !SGSimpleSettings.shared.contextShowMessageReplies { sgActions.append(actions.removeLast()) } } let isMigrated: Bool @@ -1545,6 +1574,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 { @@ -1717,18 +1747,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) @@ -1737,6 +1801,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 @@ -1855,9 +1967,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 @@ -1889,6 +2078,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) @@ -2055,7 +2278,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 554e65229c..90dab9a5cd 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -9,7 +9,7 @@ import ChatBotStartInputPanelNode import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode -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?) { var isPostSuggestions = false if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject, case .postSuggestions = customChatContents.kind { isPostSuggestions = true @@ -301,6 +301,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 9262bd3f0d..11f9fc1e45 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -508,9 +508,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?.toLang { - } else if let translateToLanguage { - self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang).startStrict()) + if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage?.toLang || translation.toLang.hasPrefix("\(translateToLanguage?.toLang ?? "")-") /* MARK: Swiftgram */ { + } else if let translateToLanguage { + self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang, viaText: !self.context.isPremium).startStrict()) } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 40818b355d..43981a1d05 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1,3 +1,9 @@ +// MARK: Swiftgram +import TelegramUIPreferences +import SGSimpleSettings +import SwiftUI +import SGInputToolbar + import Foundation import UniformTypeIdentifiers import UIKit @@ -611,6 +617,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch private let hapticFeedback = HapticFeedback() + // MARK: Swiftgram + private var sendWithReturnKey: Bool + private var sendWithReturnKeyDisposable: Disposable? +// private var toolbarHostingController: UIViewController? //Any? // UIHostingController? + private var toolbarNode: ASDisplayNode? + var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -861,6 +873,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.slowModeButton.alpha = 0.0 self.viewOnceButton = ChatRecordingViewOnceButtonNode(icon: .viewOnce) + self.sendWithReturnKey = SGUISettings.default.sendWithReturnKey super.init() @@ -894,8 +907,34 @@ 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() + } + // TODO(swiftgram): Fix call to setShowNewLine via ASDisplayNode +// if #available(iOS 13.0, *), let toolbar = strongSelf.toolbarHostingController as? UIHostingController { +// toolbar.rootView.setShowNewLine(value) +// } + } + }) + self.addSubnode(self.clippingNode) + // MARK: Swiftgram + self.initToolbarIfNeeded(context: context) + self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self else { return @@ -947,6 +986,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) @@ -1114,6 +1157,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch deinit { self.statusDisposable.dispose() + self.sendWithReturnKeyDisposable?.dispose() self.tooltipController?.dismiss() self.currentEmojiSuggestion?.disposable.dispose() } @@ -1166,6 +1210,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 if let textInputBackgroundTapRecognizer = self.textInputBackgroundTapRecognizer { @@ -1610,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 @@ -2087,7 +2132,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if buttonTitleUpdated && !transition.isAnimated { transition = .animated(duration: 0.3, curve: .easeInOut) } - + // MARK: Swiftgram + let originalLeftInset = leftInset var leftInset = leftInset var textInputBackgroundWidthOffset: CGFloat = 0.0 @@ -2917,7 +2963,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch self.viewOnceButton.isHidden = true } - return panelHeight + // MARK: Swiftgram + var toolbarOffset: CGFloat = 0.0 + toolbarOffset = layoutToolbar(transition: transition, panelHeight: panelHeight, width: width, leftInset: originalLeftInset, rightInset: rightInset, displayBotStartButton: displayBotStartButton) + + return panelHeight + toolbarOffset } @objc private func slowModeButtonPressed() { @@ -3844,7 +3894,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 { @@ -4403,6 +4453,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 { @@ -4598,6 +4655,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 @@ -5036,3 +5102,110 @@ private final class BoostSlowModeButton: HighlightTrackingButtonNode { return totalSize } } + + +// MARK: Swiftgram +extension ChatTextInputPanelNode { + + func initToolbarIfNeeded(context: AccountContext) { + guard #available(iOS 13.0, *) else { return } + guard SGSimpleSettings.shared.inputToolbar else { return } + guard context.sharedContext.immediateSGStatus.status > 1 else { return } + guard self.toolbarNode == nil else { return } + let toolbarView = ChatToolbarView( + onQuote: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesQuote(strongSelf) + }, + onSpoiler: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesSpoiler(strongSelf) + }, + onBold: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesBold(strongSelf) + }, + onItalic: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesItalic(strongSelf) + }, + onMonospace: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesMonospace(strongSelf) + }, + onLink: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesLink(strongSelf) + }, + onStrikethrough: { [weak self] + in guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesStrikethrough(strongSelf) + }, + onUnderline: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesUnderline(strongSelf) + }, + onCode: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle() + strongSelf.formatAttributesCodeBlock(strongSelf) + }, + onNewLine: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.sgSetNewLine() + }, + // TODO(swiftgram): Binding + showNewLine: .constant(true), //.constant(self.sendWithReturnKey) + onClearFormatting: { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddFormattingAttribute(forceRemoveAll: true, current, attribute: ChatTextInputAttributes.allAttributes[0], value: nil), inputMode) + } + } + ) + let toolbarHostingController = UIHostingController(rootView: toolbarView) + toolbarHostingController.view.backgroundColor = .clear + let toolbarNode = ASDisplayNode { toolbarHostingController.view } + self.toolbarNode = toolbarNode + // assigning toolbarHostingController bugs responsivness and overrides layout + // self.toolbarHostingController = toolbarHostingController + + // Disable "Swipe to go back" gesture when touching scrollview + self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in + if let self, let _ = self.toolbarNode?.view.hitTest(point, with: nil) { + return false + } + return true + } + self.addSubnode(toolbarNode) + } + + func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, displayBotStartButton: Bool) -> CGFloat { + var toolbarHeight: CGFloat = 0.0 + var toolbarSpacing: CGFloat = 0.0 + if let toolbarNode = self.toolbarNode { + if displayBotStartButton { + toolbarNode.view.isHidden = true + /*} else if !self.isFocused { + transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { _ in + toolbarNode.isHidden = true + })*/ + } else { + toolbarHeight = 44.0 + toolbarSpacing = 1.0 + // toolbarNode.isHidden = false + transition.updateFrame(node: toolbarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight))) + // transition.updateAlpha(node: toolbarNode, alpha: 1.0) + } + } + return toolbarHeight + toolbarSpacing + } +} diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index add15899cf..045481302f 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 @@ -196,7 +197,7 @@ final class ChatTranslationPanelNode: ASDisplayNode { guard let translationState = self.chatInterfaceState?.translationState else { return } - + // MARK: Swiftgram let isPremium = self.chatInterfaceState?.isPremium ?? false var translationAvailable = isPremium @@ -204,7 +205,7 @@ final class ChatTranslationPanelNode: ASDisplayNode { translationAvailable = true } - if translationAvailable { + if translationAvailable || true { self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated) } else if !translationState.isEnabled { if !isPremium { diff --git a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift index f13e9ea36f..0acdb90d01 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift @@ -32,7 +32,7 @@ private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(context: AccountContext, presentationData: PresentationData, inverted: Bool, setPeerIdRevealed: @escaping (EnginePeer.Id?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (EnginePeer.Id) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, inverted: Bool, setPeerIdRevealed: @escaping (EnginePeer.Id?) -> Void, peerSelected: @escaping (EnginePeer, Bool) -> Void, removeRequested: @escaping (EnginePeer.Id) -> Void) -> ListViewItem { return MentionChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), inverted: inverted, peer: self.peer._asPeer(), revealed: self.revealed, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested) } } @@ -43,7 +43,7 @@ private struct CommandChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, inverted: Bool, forceUpdate: Bool, setPeerIdRevealed: @escaping (EnginePeer.Id?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (EnginePeer.Id) -> Void) -> CommandChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [MentionChatInputContextPanelEntry], to toEntries: [MentionChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, inverted: Bool, forceUpdate: Bool, setPeerIdRevealed: @escaping (EnginePeer.Id?) -> Void, peerSelected: @escaping (EnginePeer, Bool) -> Void, removeRequested: @escaping (EnginePeer.Id) -> Void) -> CommandChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -121,7 +121,7 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { strongSelf.revealedPeerId = peerId strongSelf.updateResults(strongSelf.currentResults) } - }, peerSelected: { [weak self] peer in + }, peerSelected: { [weak self] peer, mentionNext in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { switch strongSelf.mode { case .input: @@ -138,7 +138,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) if let addressName = peer.addressName, !addressName.isEmpty { - let replacementText = addressName + " " + // MARK: Swiftgram + let replacementText = addressName + (mentionNext ? " @" : " ") inputText.replaceCharacters(in: range, with: replacementText) @@ -148,7 +149,8 @@ final class MentionChatInputContextPanelNode: ChatInputContextPanelNode { } else if !peer.compactDisplayTitle.isEmpty { let replacementText = NSMutableAttributedString() replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)])) - replacementText.append(NSAttributedString(string: " ")) + // MARK: Swiftgram + replacementText.append(NSAttributedString(string: mentionNext ? " @" : " ")) let updatedRange = NSRange(location: range.location - 1, length: range.length + 1) diff --git a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift index 5c128d945e..a729c94575 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift @@ -17,13 +17,13 @@ final class MentionChatInputPanelItem: ListViewItem { fileprivate let revealed: Bool fileprivate let inverted: Bool fileprivate let peer: Peer - private let peerSelected: (EnginePeer) -> Void + let peerSelected: (EnginePeer, Bool) -> Void fileprivate let setPeerIdRevealed: (EnginePeer.Id?) -> Void fileprivate let removeRequested: (EnginePeer.Id) -> Void let selectable: Bool = true - public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: Peer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (PeerId) -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: Peer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (EnginePeer, Bool) -> Void, removeRequested: @escaping (PeerId) -> Void) { self.context = context self.presentationData = presentationData self.inverted = inverted @@ -85,14 +85,14 @@ final class MentionChatInputPanelItem: ListViewItem { if self.revealed { self.setPeerIdRevealed(nil) } else { - self.peerSelected(EnginePeer(self.peer)) + self.peerSelected(EnginePeer(self.peer), false) } } } private let avatarFont = avatarPlaceholderFont(size: 16.0) -final class MentionChatInputPanelItemNode: ListViewItemNode { +final class MentionChatInputPanelItemNode: ListViewItemNode, UIGestureRecognizerDelegate { static let itemHeight: CGFloat = 42.0 private let avatarNode: AvatarNode @@ -147,7 +147,16 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) self.recognizer = recognizer recognizer.allowAnyDirection = false + // MARK: Swiftgram + recognizer.delegate = self + // self.view.addGestureRecognizer(recognizer) + + // MARK: Swiftgram + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressed(_:))) + longPressRecognizer.minimumPressDuration = 0.3 + longPressRecognizer.delegate = self + self.view.addGestureRecognizer(longPressRecognizer) } override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { @@ -328,11 +337,13 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if let recognizer = self.recognizer, otherGestureRecognizer == recognizer { + if gestureRecognizer is ItemListRevealOptionsGestureRecognizer && otherGestureRecognizer is UILongPressGestureRecognizer { return true - } else { - return false } + if gestureRecognizer is UILongPressGestureRecognizer && otherGestureRecognizer is ItemListRevealOptionsGestureRecognizer { + return true + } + return false } @objc func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { @@ -473,3 +484,21 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.hapticFeedback?.impact(.medium) } } + + + + + +// MARK: Swiftgram +extension MentionChatInputPanelItemNode { + @objc private func longPressed(_ gestureRecognizer: UILongPressGestureRecognizer) { + switch gestureRecognizer.state { + case .began: + if let item = self.item { + item.peerSelected(EnginePeer(item.peer), true) + } + default: + break + } + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 9973541700..d899402b06 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 c440cb496f..a60272e55e 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1,3 +1,12 @@ +import SGLogging +import SGAPIWebSettings +import SGConfig +import SGSettingsUI +import SGDebugUI +import SFSafariViewControllerPlus +import UndoUI +// +import ContactListUI import Foundation import Display import SafariServices @@ -1018,6 +1027,71 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } else { if parsedUrl.host == "stars" { handleResolvedUrl(.stars) + } else 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 + case "contacts": + if let lastViewController = navigationController?.viewControllers.last as? ViewController { + lastViewController.present(ContactsController(context: context), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + return + case "pro", "premium", "buy": + if context.sharedContext.immediateSGStatus.status > 1 { + navigationController?.pushViewController(context.sharedContext.makeSGProController(context: context)) + } else { + if let lastViewController = navigationController?.viewControllers.last as? ViewController { + if let payWallController = context.sharedContext.makeSGPayWallController(context: context) { + lastViewController.present(payWallController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + lastViewController.present(context.sharedContext.makeSGUpdateIOSController(), animated: true) + } + } + } + case "restart": + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let lang = presentationData.strings.baseLanguageCode + context.sharedContext.presentGlobalController( + UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, + text: "Common.RestartRequired".i18n(lang), + timeout: nil, + customUndoText: "Common.RestartNow".i18n(lang) + ), + elevatedLayout: false, + action: { action in if action == .undo { exit(0) }; return true } + ), + nil + ) + case "restore_purchases", "pro_restore", "validate", "restore": + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let lang = presentationData.strings.baseLanguageCode + context.sharedContext.presentGlobalController(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(lang), timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ), + nil) + context.sharedContext.SGIAP?.restorePurchases {} + default: + break + } + } } else if parsedUrl.host == "importStickers" { handleResolvedUrl(.importStickers) } else if parsedUrl.host == "settings" { @@ -1137,15 +1211,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/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 365fca7ed3..45f72835c1 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1,3 +1,9 @@ +// MARK: Swiftgram +import SGIAP +import SGPayWall +import SGProUI +import SGSimpleSettings +// import Foundation import UIKit import AsyncDisplayKit @@ -253,6 +259,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return self.immediateExperimentalUISettingsValue.with { $0 } } private var experimentalUISettingsDisposable: Disposable? + + // MARK: Swiftgram + private var immediateSGStatusValue = Atomic(value: SGStatus.default) + public var immediateSGStatus: SGStatus { + return self.immediateSGStatusValue.with { $0 } + } + private var sgStatusDisposable: Disposable? + public var SGIAP: SGIAPManager? public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in } public var presentCrossfadeController: () -> Void = {} @@ -483,6 +497,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { flatBuffers_checkedGet = settings.checkSerializedData } }) + // MARK: Swiftgram + let immediateSGStatusValue = self.immediateSGStatusValue + self.sgStatusDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus]) + |> deliverOnMainQueue).start(next: { sharedData in + if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) { + let _ = immediateSGStatusValue.swap(settings) + SGSimpleSettings.shared.ephemeralStatus = settings.status + SGSimpleSettings.shared.status = settings.status + } + }) + self.initSGIAP(isMainApp: applicationBindings.isMainApp) + // let _ = self.contactDataManager?.personNameDisplayOrder().start(next: { order in let _ = updateContactSettingsInteractively(accountManager: accountManager, { settings in @@ -3923,3 +3949,68 @@ private func useFlatModalCallsPresentation(context: AccountContext) -> Bool { } return true } + + + +// MARK: Swiftgram +extension SharedAccountContextImpl { + func initSGIAP(isMainApp: Bool) { + if isMainApp { + self.SGIAP = SGIAPManager() + } else { + self.SGIAP = nil + } + } + + public func makeSGProController(context: AccountContext) -> ViewController { + let controller = sgProController(context: context) + return controller + } + + public func makeSGPayWallController(context: AccountContext) -> ViewController? { + guard #available(iOS 13.0, *) else { + return nil + } + guard let sgIAP = self.SGIAP else { + return nil + } + + let statusSignal = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus]) + |> map { sharedData -> Int64 in + let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default + return sgStatus.status + } + + let proController = self.makeSGProController(context: context) + let sgWebSettings = context.currentAppConfiguration.with { $0 }.sgWebSettings + let presentationData = self.currentPresentationData.with { $0 } + var payWallController: ViewController? = nil + let openUrl: ((String, Bool) -> Void) = { [weak self, weak context] url, forceExternal in + guard let strongSelf = self, let strongContext = context, let strongPayWallController = payWallController else { + return + } + let navigationController = strongPayWallController.navigationController as? NavigationController + Queue.mainQueue().async { + strongSelf.openExternalUrl(context: strongContext, urlContext: .generic, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } + + var supportUrl: String? = nil + if let supportUrlString = sgWebSettings.global.proSupportUrl, !supportUrlString.isEmpty, let data = Data(base64Encoded: supportUrlString), let decodedString = String(data: data, encoding: .utf8) { + supportUrl = decodedString + } + payWallController = sgPayWallController(statusSignal: statusSignal, replacementController: proController, presentationData: presentationData, SGIAPManager: sgIAP, openUrl: openUrl, paymentsEnabled: sgWebSettings.global.paymentsEnabled, canBuyInBeta: sgWebSettings.user.canBuyInBeta, openAppStorePage: self.applicationBindings.openAppStorePage, proSupportUrl: supportUrl) + return payWallController + } + + public func makeSGUpdateIOSController() -> ViewController { + let presentationData = self.currentPresentationData.with { $0 } + let controller = UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: "Common.UpdateOS".i18n(presentationData.strings.baseLanguageCode), timeout: nil, customUndoText: nil), + elevatedLayout: false, + action: { _ in return false } + ) + return controller + } +} diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index c1e99fec5d..9b7bd66ca4 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!) } @@ -671,7 +681,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: 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): @@ -694,7 +704,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: 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 51b7b85aac..aa94c8b521 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/BUILD b/submodules/TelegramUIPreferences/BUILD index 5a106b25ca..e18637975c 100644 --- a/submodules/TelegramUIPreferences/BUILD +++ b/submodules/TelegramUIPreferences/BUILD @@ -1,9 +1,13 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +sgsrcs = [ + "//Swiftgram/SGStatus:SGStatus" +] + swift_library( name = "TelegramUIPreferences", module_name = "TelegramUIPreferences", - srcs = glob([ + srcs = sgsrcs + glob([ "Sources/**/*.swift", ]), copts = [ diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index ec477ff08c..1fa98daef6 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) @@ -19,6 +21,8 @@ public struct ApplicationSpecificPreferencesKeys { } private enum ApplicationSpecificSharedDataKeyValues: Int32 { + // MARK: Swiftgram + case sgStatus = 999 case inAppNotificationSettings = 0 case presentationPasscodeSettings = 1 case automaticMediaDownloadSettings = 2 @@ -43,6 +47,8 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { } public struct ApplicationSpecificSharedDataKeys { + // MARK: Swiftgram + public static let sgStatus = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.sgStatus.rawValue) public static let inAppNotificationSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.inAppNotificationSettings.rawValue) public static let presentationPasscodeSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.presentationPasscodeSettings.rawValue) public static let automaticMediaDownloadSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.automaticMediaDownloadSettings.rawValue) diff --git a/submodules/TelegramUIPreferences/Sources/Swiftgram/SGUISettings.swift b/submodules/TelegramUIPreferences/Sources/Swiftgram/SGUISettings.swift new file mode 100644 index 0000000000..c6a0054f40 --- /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 6de2b55b35..3ab31455e4 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 98b317cd03..068575d07a 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 @@ -54,6 +55,15 @@ public struct ChatTranslationState: Codable { try container.encode(self.isEnabled, forKey: .isEnabled) } + public func withFromLang(_ fromLang: String) -> ChatTranslationState { + return ChatTranslationState( + baseLang: self.baseLang, + fromLang: fromLang, + timestamp: self.timestamp, + toLang: self.toLang, + isEnabled: self.isEnabled + ) + } public func withToLang(_ toLang: String?) -> ChatTranslationState { return ChatTranslationState( baseLang: self.baseLang, @@ -138,8 +148,9 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer @available(iOS 12.0, *) private let languageRecognizer = NLLanguageRecognizer() -public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], fromLang: String?, toLang: String) -> Signal { +public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], fromLang: String?, 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 { @@ -151,11 +162,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 { @@ -166,8 +179,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) @@ -180,14 +195,24 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess } } } + if viaText { + return context.engine.messages.translateMessagesViaText(messagesDict: messageDictToTranslate, fromLang: fromLang, toLang: toLang, generateEntitiesFunction: { text in + generateTextEntities(text, enabledTypes: .all) + }, enableLocalIfPossible: context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation) + |> `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, fromLang: fromLang, toLang: toLang, enableLocalIfPossible: context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation) |> `catch` { _ -> Signal in return .complete() } + } } |> switchToLatest } -public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64?) -> Signal { +public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64?, forcePredict: Bool = false) -> Signal { if peerId.id == EnginePeer.Id.Id._internalFromInt64Value(777000) { return .single(nil) } @@ -209,7 +234,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId)) ) |> mapToSignal { settings, autoTranslateEnabled in - if !settings.translateChats && !autoTranslateEnabled { + if !settings.translateChats && !autoTranslateEnabled && !forcePredict { return .single(nil) } @@ -227,7 +252,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, |> mapToSignal { cached in let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 { - if !dontTranslateLanguages.contains(cached.fromLang) { + if !dontTranslateLanguages.contains(cached.fromLang) || forcePredict { return .single(cached) } else { return .single(nil) @@ -330,7 +355,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id, isEnabled: isEnabled ) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, threadId: threadId, 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 a9d2e4ea86..07c488f105 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/WatchBridge/Sources/WatchBridge.swift b/submodules/WatchBridge/Sources/WatchBridge.swift index 8f88b40ff7..e70982631e 100644 --- a/submodules/WatchBridge/Sources/WatchBridge.swift +++ b/submodules/WatchBridge/Sources/WatchBridge.swift @@ -18,7 +18,9 @@ func makePeerIdFromBridgeIdentifier(_ identifier: Int64) -> PeerId? { return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(-identifier)) } else if identifier < Int64(Int32.min) * 2 && identifier > Int64(Int32.min) * 3 { return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(Int64(Int32.min) &* 2 &- identifier)) - } else if identifier > 0 && identifier < Int32.max { + // MARK: Swiftgram + // supports 52 bits + } else if identifier > 0 && identifier < (1 << 52) { return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(identifier)) } else { return nil diff --git a/submodules/WatchCommon/Host/PublicHeaders/WatchCommon/TGBridgePeerIdAdapter.h b/submodules/WatchCommon/Host/PublicHeaders/WatchCommon/TGBridgePeerIdAdapter.h index c5f0ac92fc..5c646d56dd 100644 --- a/submodules/WatchCommon/Host/PublicHeaders/WatchCommon/TGBridgePeerIdAdapter.h +++ b/submodules/WatchCommon/Host/PublicHeaders/WatchCommon/TGBridgePeerIdAdapter.h @@ -1,52 +1,120 @@ #ifndef Telegraph_TGPeerIdAdapter_h #define Telegraph_TGPeerIdAdapter_h -static inline bool TGPeerIdIsGroup(int64_t peerId) { - return peerId < 0 && peerId > INT32_MIN; +// Namespace constants based on Swift implementation +#define TG_NAMESPACE_MASK 0x7 +#define TG_NAMESPACE_EMPTY 0x0 +#define TG_NAMESPACE_CLOUD 0x1 +#define TG_NAMESPACE_GROUP 0x2 +#define TG_NAMESPACE_CHANNEL 0x3 +#define TG_NAMESPACE_SECRET_CHAT 0x4 +#define TG_NAMESPACE_ADMIN_LOG 0x5 +#define TG_NAMESPACE_AD 0x6 +#define TG_NAMESPACE_MAX 0x7 + +// Helper functions for bit manipulation +static inline uint32_t TGPeerIdGetNamespace(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + return (uint32_t)((data >> 32) & TG_NAMESPACE_MASK); +} + +static inline int64_t TGPeerIdGetId(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + uint64_t idHighBits = (data >> (32 + 3)) << 32; + uint64_t idLowBits = data & 0xffffffff; + return (int64_t)(idHighBits | idLowBits); +} + +static inline int64_t TGPeerIdMake(uint32_t namespaceId, int64_t id) { + uint64_t data = 0; + uint64_t idBits = (uint64_t)id; + uint64_t idLowBits = idBits & 0xffffffff; + uint64_t idHighBits = (idBits >> 32) & 0xffffffff; + + data |= ((uint64_t)(namespaceId & TG_NAMESPACE_MASK)) << 32; + data |= (idHighBits << (32 + 3)); + data |= idLowBits; + + return (int64_t)data; +} + +// Updated peer type checks +static inline bool TGPeerIdIsEmpty(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_EMPTY; } static inline bool TGPeerIdIsUser(int64_t peerId) { - return peerId > 0 && peerId < INT32_MAX; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CLOUD; +} + +static inline bool TGPeerIdIsGroup(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_GROUP; } static inline bool TGPeerIdIsChannel(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 2 && peerId > ((int64_t)INT32_MIN) * 3; -} - -static inline bool TGPeerIdIsAdminLog(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 3 && peerId > ((int64_t)INT32_MIN) * 4; -} - -static inline int32_t TGChannelIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsChannel(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 2 - peerId); - } else { - return 0; - } -} - -static inline int64_t TGPeerIdFromChannelId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 2 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromAdminLogId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 3 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromGroupId(int32_t groupId) { - return -groupId; -} - -static inline int32_t TGGroupIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsGroup(peerId)) { - return (int32_t)-peerId; - } else { - return 0; - } + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CHANNEL; } static inline bool TGPeerIdIsSecretChat(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) && peerId > ((int64_t)INT32_MIN) * 2; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_SECRET_CHAT; +} + +static inline bool TGPeerIdIsAdminLog(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_ADMIN_LOG; +} + +static inline bool TGPeerIdIsAd(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_AD; +} + +// Conversion functions +static inline int64_t TGPeerIdFromUserId(int64_t userId) { + return TGPeerIdMake(TG_NAMESPACE_CLOUD, userId); +} + +static inline int64_t TGPeerIdFromGroupId(int64_t groupId) { + return TGPeerIdMake(TG_NAMESPACE_GROUP, groupId); +} + +static inline int64_t TGPeerIdFromChannelId(int64_t channelId) { + return TGPeerIdMake(TG_NAMESPACE_CHANNEL, channelId); +} + +static inline int64_t TGPeerIdFromSecretChatId(int64_t secretChatId) { + return TGPeerIdMake(TG_NAMESPACE_SECRET_CHAT, secretChatId); +} + +static inline int64_t TGPeerIdFromAdminLogId(int64_t adminLogId) { + return TGPeerIdMake(TG_NAMESPACE_ADMIN_LOG, adminLogId); +} + +static inline int64_t TGPeerIdFromAdId(int64_t adId) { + return TGPeerIdMake(TG_NAMESPACE_AD, adId); +} + +// Extract IDs +static inline int64_t TGUserIdFromPeerId(int64_t peerId) { + return TGPeerIdIsUser(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGGroupIdFromPeerId(int64_t peerId) { + return TGPeerIdIsGroup(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGChannelIdFromPeerId(int64_t peerId) { + return TGPeerIdIsChannel(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGSecretChatIdFromPeerId(int64_t peerId) { + return TGPeerIdIsSecretChat(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdminLogIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAdminLog(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAd(peerId) ? TGPeerIdGetId(peerId) : 0; } #endif diff --git a/submodules/WatchCommon/Watch/Sources/TGBridgePeerIdAdapter.h b/submodules/WatchCommon/Watch/Sources/TGBridgePeerIdAdapter.h index c5f0ac92fc..5c646d56dd 100644 --- a/submodules/WatchCommon/Watch/Sources/TGBridgePeerIdAdapter.h +++ b/submodules/WatchCommon/Watch/Sources/TGBridgePeerIdAdapter.h @@ -1,52 +1,120 @@ #ifndef Telegraph_TGPeerIdAdapter_h #define Telegraph_TGPeerIdAdapter_h -static inline bool TGPeerIdIsGroup(int64_t peerId) { - return peerId < 0 && peerId > INT32_MIN; +// Namespace constants based on Swift implementation +#define TG_NAMESPACE_MASK 0x7 +#define TG_NAMESPACE_EMPTY 0x0 +#define TG_NAMESPACE_CLOUD 0x1 +#define TG_NAMESPACE_GROUP 0x2 +#define TG_NAMESPACE_CHANNEL 0x3 +#define TG_NAMESPACE_SECRET_CHAT 0x4 +#define TG_NAMESPACE_ADMIN_LOG 0x5 +#define TG_NAMESPACE_AD 0x6 +#define TG_NAMESPACE_MAX 0x7 + +// Helper functions for bit manipulation +static inline uint32_t TGPeerIdGetNamespace(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + return (uint32_t)((data >> 32) & TG_NAMESPACE_MASK); +} + +static inline int64_t TGPeerIdGetId(int64_t peerId) { + uint64_t data = (uint64_t)peerId; + uint64_t idHighBits = (data >> (32 + 3)) << 32; + uint64_t idLowBits = data & 0xffffffff; + return (int64_t)(idHighBits | idLowBits); +} + +static inline int64_t TGPeerIdMake(uint32_t namespaceId, int64_t id) { + uint64_t data = 0; + uint64_t idBits = (uint64_t)id; + uint64_t idLowBits = idBits & 0xffffffff; + uint64_t idHighBits = (idBits >> 32) & 0xffffffff; + + data |= ((uint64_t)(namespaceId & TG_NAMESPACE_MASK)) << 32; + data |= (idHighBits << (32 + 3)); + data |= idLowBits; + + return (int64_t)data; +} + +// Updated peer type checks +static inline bool TGPeerIdIsEmpty(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_EMPTY; } static inline bool TGPeerIdIsUser(int64_t peerId) { - return peerId > 0 && peerId < INT32_MAX; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CLOUD; +} + +static inline bool TGPeerIdIsGroup(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_GROUP; } static inline bool TGPeerIdIsChannel(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 2 && peerId > ((int64_t)INT32_MIN) * 3; -} - -static inline bool TGPeerIdIsAdminLog(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) * 3 && peerId > ((int64_t)INT32_MIN) * 4; -} - -static inline int32_t TGChannelIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsChannel(peerId)) { - return (int32_t)(((int64_t)INT32_MIN) * 2 - peerId); - } else { - return 0; - } -} - -static inline int64_t TGPeerIdFromChannelId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 2 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromAdminLogId(int32_t channelId) { - return ((int64_t)INT32_MIN) * 3 - ((int64_t)channelId); -} - -static inline int64_t TGPeerIdFromGroupId(int32_t groupId) { - return -groupId; -} - -static inline int32_t TGGroupIdFromPeerId(int64_t peerId) { - if (TGPeerIdIsGroup(peerId)) { - return (int32_t)-peerId; - } else { - return 0; - } + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_CHANNEL; } static inline bool TGPeerIdIsSecretChat(int64_t peerId) { - return peerId <= ((int64_t)INT32_MIN) && peerId > ((int64_t)INT32_MIN) * 2; + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_SECRET_CHAT; +} + +static inline bool TGPeerIdIsAdminLog(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_ADMIN_LOG; +} + +static inline bool TGPeerIdIsAd(int64_t peerId) { + return TGPeerIdGetNamespace(peerId) == TG_NAMESPACE_AD; +} + +// Conversion functions +static inline int64_t TGPeerIdFromUserId(int64_t userId) { + return TGPeerIdMake(TG_NAMESPACE_CLOUD, userId); +} + +static inline int64_t TGPeerIdFromGroupId(int64_t groupId) { + return TGPeerIdMake(TG_NAMESPACE_GROUP, groupId); +} + +static inline int64_t TGPeerIdFromChannelId(int64_t channelId) { + return TGPeerIdMake(TG_NAMESPACE_CHANNEL, channelId); +} + +static inline int64_t TGPeerIdFromSecretChatId(int64_t secretChatId) { + return TGPeerIdMake(TG_NAMESPACE_SECRET_CHAT, secretChatId); +} + +static inline int64_t TGPeerIdFromAdminLogId(int64_t adminLogId) { + return TGPeerIdMake(TG_NAMESPACE_ADMIN_LOG, adminLogId); +} + +static inline int64_t TGPeerIdFromAdId(int64_t adId) { + return TGPeerIdMake(TG_NAMESPACE_AD, adId); +} + +// Extract IDs +static inline int64_t TGUserIdFromPeerId(int64_t peerId) { + return TGPeerIdIsUser(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGGroupIdFromPeerId(int64_t peerId) { + return TGPeerIdIsGroup(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGChannelIdFromPeerId(int64_t peerId) { + return TGPeerIdIsChannel(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGSecretChatIdFromPeerId(int64_t peerId) { + return TGPeerIdIsSecretChat(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdminLogIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAdminLog(peerId) ? TGPeerIdGetId(peerId) : 0; +} + +static inline int64_t TGAdIdFromPeerId(int64_t peerId) { + return TGPeerIdIsAd(peerId) ? TGPeerIdGetId(peerId) : 0; } #endif diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 29f65144f9..6f52b88aa8 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, hasCoverButton: 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, hasCoverButton: false)! model.stickersContext = paintStickersContext controller.model = model model.controller = controller diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 3c807f17c0..b52acd073f 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 dda6079112..78a0f5d3b9 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 @@ -197,7 +200,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 @@ -214,7 +217,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 @@ -3274,6 +3286,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, Bool, @escaping () -> Void) -> Void = { _, _, _, _ in } @@ -3605,6 +3618,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) @@ -3705,6 +3731,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 ad3ab346ab..ffc703ec1c 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -104,7 +104,7 @@ final class WebAppWebView: WKWebView { return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right) } - 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 @@ -268,6 +272,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() @@ -282,3 +289,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 + } +}