diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7259ebd7e73..f504b663f71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,8 @@ name: CI on: - push: - branches: [ master ] + # push: + # branches: [ master ] workflow_dispatch: diff --git a/.gitignore b/.gitignore index 4a8af5793f1..6942b9417e6 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 e66c7387a0e..4e2b9b3a563 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/launch.json b/.vscode/launch.json index 9c36c383b3a..a65fa9d0118 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,22 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder}", + "name": "Run Swiftgram", + "preLaunchTask": "sweetpad: launch" + }, + { + "type": "swift", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder}", + "name": "Debug Swiftgram", + "preLaunchTask": "sweetpad: debugging-launch" + }, { "type": "swift", "request": "launch", diff --git a/.vscode/settings.json b/.vscode/settings.json index 3090f4c8be3..5d478762dbe 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/MODULE.bazel b/MODULE.bazel index a9aae97daf1..ba7f24ba441 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,4 +1,6 @@ http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +# MARK: Swiftgram +new_git_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") bazel_dep(name = "bazel_features", version = "1.30.0") bazel_dep(name = "bazel_skylib", version = "1.7.1") @@ -28,6 +30,14 @@ local_path_override( path = "./build-system/bazel-rules/apple_support", ) +# MARK: Swiftgram +new_git_repository( + name = "flex_sdk", + remote = "https://github.com/FLEXTool/FLEX.git", + commit = "2bfba6715eff664ef84a02e8eb0ad9b5a609c684", + build_file = "@//Swiftgram/FLEX:FLEX.BUILD" +) + http_file( name = "cmake_tar_gz", urls = ["https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1-macos-universal.tar.gz"], diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ed86535e8b1..c8f2a8c8b2c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -159,7 +159,7 @@ "moduleExtensions": { "@@apple_support+//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", + "bzlTransitiveDigest": "xcBTf2+GaloFpg7YEh/Bv+1yAczRkiCt3DGws4K7kSk=", "usagesDigest": "lfcV4HxPD+NLaRIT/v7BtSGFgE7c9xrWU7jDiwBAxzo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/README.md b/README.md index 79f325aa139..1f754271a88 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# Swiftgram + +Supercharged Telegram fork for iOS + +[](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8) + +- Download: [App Store](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8) +- Telegram channel: https://t.me/swiftgram +- Telegram chat: https://t.me/swiftgramchat +- TestFlight beta, local chats, translations and other [@SwiftgramLinks](https://t.me/s/SwiftgramLinks) + +Swiftgram's compilation steps are the same as for the official app. Below you'll find a complete compilation guide based on the official app. + # Telegram iOS Source Code Compilation Guide We welcome all developers to use our API and source code to create applications on our platform. @@ -16,7 +29,7 @@ There are several things we require from **all developers** for the moment. ## Get the Code ``` -git clone --recursive -j8 https://github.com/TelegramMessenger/Telegram-iOS.git +git clone --recursive -j8 https://github.com/Swiftgram/Telegram-iOS.git ``` ## Setup Xcode @@ -29,7 +42,7 @@ Install Xcode (directly from https://developer.apple.com/download/applications o ``` openssl rand -hex 8 ``` -2. Create a new Xcode project. Use `Telegram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier. +2. Create a new Xcode project. Use `Swiftgram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier. 3. Open `Keychain Access` and navigate to `Certificates`. Locate `Apple Development: your@email.address (XXXXXXXXXX)` and double tap the certificate. Under `Details`, locate `Organizational Unit`. This is the Team ID. 4. Edit `build-system/template_minimal_development_configuration.json`. Use data from the previous steps. diff --git a/Swiftgram/AppleStyleFolders/BUILD b/Swiftgram/AppleStyleFolders/BUILD new file mode 100644 index 00000000000..0924cf28e86 --- /dev/null +++ b/Swiftgram/AppleStyleFolders/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "AppleStyleFolders", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/AppleStyleFolders/Sources/File.swift b/Swiftgram/AppleStyleFolders/Sources/File.swift new file mode 100644 index 00000000000..c2ef2cb59e0 --- /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 00000000000..15c650e14a6 --- /dev/null +++ b/Swiftgram/ChatControllerImplExtension/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "ChatControllerImplExtension", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift b/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift new file mode 100644 index 00000000000..23d5c46b4c2 --- /dev/null +++ b/Swiftgram/ChatControllerImplExtension/Sources/ChatControllerImplExtension.swift @@ -0,0 +1,225 @@ +import SGSimpleSettings +import Foundation +import UIKit +import Postbox +import SwiftSignalKit +import Display +import AsyncDisplayKit +import TelegramCore +import SafariServices +import MobileCoreServices +import Intents +import LegacyComponents +import TelegramPresentationData +import TelegramUIPreferences +import DeviceAccess +import TextFormat +import TelegramBaseController +import AccountContext +import TelegramStringFormatting +import OverlayStatusController +import DeviceLocationManager +import ShareController +import UrlEscaping +import ContextUI +import ComposePollUI +import AlertUI +import PresentationDataUtils +import UndoUI +import TelegramCallsUI +import TelegramNotices +import GameUI +import ScreenCaptureDetection +import GalleryUI +import OpenInExternalAppUI +import LegacyUI +import InstantPageUI +import LocationUI +import BotPaymentsUI +import DeleteChatPeerActionSheetItem +import HashtagSearchUI +import LegacyMediaPickerUI +import Emoji +import PeerAvatarGalleryUI +import PeerInfoUI +import RaiseToListen +import UrlHandling +import AvatarNode +import AppBundle +import LocalizedPeerData +import PhoneNumberFormat +import SettingsUI +import UrlWhitelist +import TelegramIntents +import TooltipUI +import StatisticsUI +import MediaResources +import GalleryData +import ChatInterfaceState +import InviteLinksUI +import Markdown +import TelegramPermissionsUI +import Speak +import TranslateUI +import UniversalMediaPlayer +import WallpaperBackgroundNode +import ChatListUI +import CalendarMessageScreen +import ReactionSelectionNode +import ReactionListContextMenuContent +import AttachmentUI +import AttachmentTextInputPanelNode +import MediaPickerUI +import ChatPresentationInterfaceState +import Pasteboard +import ChatSendMessageActionUI +import ChatTextLinkEditUI +import WebUI +import PremiumUI +import ImageTransparency +import StickerPackPreviewUI +import TextNodeWithEntities +import EntityKeyboard +import ChatTitleView +import EmojiStatusComponent +import ChatTimerScreen +import MediaPasteboardUI +import ChatListHeaderComponent +import ChatControllerInteraction +import FeaturedStickersScreen +import ChatEntityKeyboardInputNode +import StorageUsageScreen +import AvatarEditorScreen +import ChatScheduleTimeController +import ICloudResources +import StoryContainerScreen +import MoreHeaderButton +import VolumeButtons +import ChatAvatarNavigationNode +import ChatContextQuery +import PeerReportScreen +import PeerSelectionController +import SaveToCameraRoll +import ChatMessageDateAndStatusNode +import ReplyAccessoryPanelNode +import TextSelectionNode +import ChatMessagePollBubbleContentNode +import ChatMessageItem +import ChatMessageItemImpl +import ChatMessageItemView +import ChatMessageItemCommon +import ChatMessageAnimatedStickerItemNode +import ChatMessageBubbleItemNode +import ChatNavigationButton +import WebsiteType +import ChatQrCodeScreen +import PeerInfoScreen +import MediaEditorScreen +import WallpaperGalleryScreen +import WallpaperGridScreen +import VideoMessageCameraScreen +import TopMessageReactions +import AudioWaveform +import PeerNameColorScreen +import ChatEmptyNode +import ChatMediaInputStickerGridItem +import AdsInfoScreen + +extension ChatControllerImpl { + + func forwardMessagesToCloud(messageIds: [MessageId], removeNames: Bool, openCloud: Bool, resetCurrent: Bool = false) { + let _ = (self.context.engine.data.get(EngineDataMap( + messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) + )) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + guard let strongSelf = self else { + return + } + + if resetCurrent { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withoutSelectionState() }) }) + } + + let sortedMessages = messages.values.compactMap { $0?._asMessage() }.sorted { lhs, rhs in + return lhs.id < rhs.id + } + + var attributes: [MessageAttribute] = [] + if removeNames { + attributes.append(ForwardOptionsMessageAttribute(hideNames: true, hideCaptions: false)) + } + + if !openCloud { + Queue.mainQueue().after(0.88) { + strongSelf.chatDisplayNode.hapticFeedback.success() + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in + if case .info = value, let strongSelf = self { + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let strongSelf = self, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else { + return + } + + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + return true + } + return false + }), in: .current) + } + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.context.account.peerId, messages: sortedMessages.map { message -> EnqueueMessage in + return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil) + }) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + guard openCloud else { + return + } + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] _ in + guard let strongSelf = strongSelf else { + return + } + strongSelf.chatDisplayNode.hapticFeedback.success() + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] peer in + guard let strongSelf = strongSelf, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else { + return + } + + var navigationSubject: ChatControllerSubject? = nil + for messageId in messageIds { + if let messageId = messageId { + navigationSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) + break + } + } + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: navigationSubject, keepStack: .always, purposefulAction: {}, peekData: nil)) + }) + } )) + } + }) + }) + } +} diff --git a/Swiftgram/FLEX/BUILD b/Swiftgram/FLEX/BUILD new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Swiftgram/FLEX/FLEX.BUILD b/Swiftgram/FLEX/FLEX.BUILD new file mode 100644 index 00000000000..52e69f69169 --- /dev/null +++ b/Swiftgram/FLEX/FLEX.BUILD @@ -0,0 +1,68 @@ +objc_library( + name = "FLEX", + module_name = "FLEX", + srcs = glob( + ["Classes/**/*"], + exclude = [ + "Classes/Info.plist", + "Classes/Utility/APPLE_LICENSE", + "Classes/Network/OSCache/LICENSE.md", + "Classes/Network/PonyDebugger/LICENSE", + "Classes/GlobalStateExplorers/DatabaseBrowser/LICENSE", + "Classes/GlobalStateExplorers/Keychain/SSKeychain_LICENSE", + "Classes/GlobalStateExplorers/SystemLog/LLVM_LICENSE.TXT", + ] + ), + hdrs = glob([ + "Classes/**/*.h" + ]), + includes = [ + "Classes", + "Classes/Core", + "Classes/Core/Controllers", + "Classes/Core/Views", + "Classes/Core/Views/Cells", + "Classes/Core/Views/Carousel", + "Classes/ObjectExplorers", + "Classes/ObjectExplorers/Sections", + "Classes/ObjectExplorers/Sections/Shortcuts", + "Classes/Network", + "Classes/Network/PonyDebugger", + "Classes/Network/OSCache", + "Classes/Toolbar", + "Classes/Manager", + "Classes/Manager/Private", + "Classes/Editing", + "Classes/Editing/ArgumentInputViews", + "Classes/Headers", + "Classes/ExplorerInterface", + "Classes/ExplorerInterface/Tabs", + "Classes/ExplorerInterface/Bookmarks", + "Classes/GlobalStateExplorers", + "Classes/GlobalStateExplorers/Globals", + "Classes/GlobalStateExplorers/Keychain", + "Classes/GlobalStateExplorers/FileBrowser", + "Classes/GlobalStateExplorers/SystemLog", + "Classes/GlobalStateExplorers/DatabaseBrowser", + "Classes/GlobalStateExplorers/RuntimeBrowser", + "Classes/GlobalStateExplorers/RuntimeBrowser/DataSources", + "Classes/ViewHierarchy", + "Classes/ViewHierarchy/SnapshotExplorer", + "Classes/ViewHierarchy/SnapshotExplorer/Scene", + "Classes/ViewHierarchy/TreeExplorer", + "Classes/Utility", + "Classes/Utility/Runtime", + "Classes/Utility/Runtime/Objc", + "Classes/Utility/Runtime/Objc/Reflection", + "Classes/Utility/Categories", + "Classes/Utility/Categories/Private", + "Classes/Utility/Keyboard" + ], + copts = [ + "-Wno-deprecated-declarations", + "-Wno-strict-prototypes", + "-Wno-unsupported-availability-guard", + ], + deps = [], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/Swiftgram/FixConcurrencyBackport/BUILD b/Swiftgram/FixConcurrencyBackport/BUILD new file mode 100644 index 00000000000..bebc96f67f7 --- /dev/null +++ b/Swiftgram/FixConcurrencyBackport/BUILD @@ -0,0 +1,43 @@ +# Something changed in Telegram versions 11.8.1 -> 11.10 +# https://github.com/TelegramMessenger/Telegram-iOS/compare/release-11.8.1...TelegramMessenger:Telegram-iOS:release-11.10 +# +# Since then, all binaries and libs are linked to the /usr/lib/swift/libswift_Concurrency.dylib instead of expected @rpath/libswift_Concurrency.dylib, +# this makes swift-stdlib-tool to ignore libswift_Concurrency.dylib and not copy it to the app bundle. +# This causes crash on every system that expects this backport (iOS 14 and below). +# This script will remap the path to @rpath/libswift_Concurrency.dylib in all binaries of the App, it's only needed for iphoneos target in this project. +# This is a temporary fix until minimum OS version will be bumped to iOS 15+ or Xcode version changed to 16.3 (with Swift 6.1 support) + +# find "$1" -type f \( -perm +111 -o -name "*.dylib" \) | while read -r bin; do +# if otool -L "$bin" | grep -q "/usr/lib/swift/libswift_Concurrency.dylib"; then +# echo "Patching concurrency backport in: $bin" +# install_name_tool -change /usr/lib/swift/libswift_Concurrency.dylib @rpath/libswift_Concurrency.dylib "$bin" +# fi +# done + +# concurrency-dylib.patch must be applied in build-system/bazel-rules/rules_apple +# cd Swiftgram/FixConcurrencyBackport +# git apply ../../../Swiftgram/FixConcurrencyBackport/concurrency-dylib.patch +# # Make a build +# git apply -R ../../../Swiftgram/FixConcurrencyBackport/concurrency-dylib.patch + +# Refs: +# https://stackoverflow.com/questions/79522371/when-building-the-project-with-xcode-16-2-the-app-crashes-due-to-an-incorrect-l +# https://github.com/swiftlang/swift/issues/74303 +# https://github.com/bazelbuild/rules_apple/pull/1393 + +genrule( + name = "CopyConcurrencyDylib", + cmd_bash = +""" + echo 'ditto "$$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/iphoneos/libswift_Concurrency.dylib" "$$1/Payload/Swiftgram.app/Frameworks/libswift_Concurrency.dylib"' > $(location CopyConcurrencyDylib.sh) + echo 'ditto "$$1/Payload/Swiftgram.app/Frameworks/libswift_Concurrency.dylib" "$$1/SwiftSupport/iphoneos/libswift_Concurrency.dylib"' >> $(location CopyConcurrencyDylib.sh) + echo '' >> $(location CopyConcurrencyDylib.sh) +""", + outs = [ + "CopyConcurrencyDylib.sh", + ], + executable = True, + visibility = [ + "//visibility:public", + ] +) \ No newline at end of file diff --git a/Swiftgram/FixConcurrencyBackport/concurrency-dylib.patch b/Swiftgram/FixConcurrencyBackport/concurrency-dylib.patch new file mode 100644 index 00000000000..794749f79af --- /dev/null +++ b/Swiftgram/FixConcurrencyBackport/concurrency-dylib.patch @@ -0,0 +1,25 @@ +diff --git a/tools/swift_stdlib_tool/swift_stdlib_tool.py b/tools/swift_stdlib_tool/swift_stdlib_tool.py +index fbb7f4fb..5a2277c5 100644 +--- a/tools/swift_stdlib_tool/swift_stdlib_tool.py ++++ b/tools/swift_stdlib_tool/swift_stdlib_tool.py +@@ -134,6 +134,20 @@ def _copy_swift_stdlibs(binaries_to_scan, sdk_platform, destination_path): + if os.path.exists(libswiftcore_path): + os.remove(libswiftcore_path) + ++ # MARK: Swiftgram ++ if sdk_platform == "iphoneos": ++ # Copy the concurrency runtime to the destination path. ++ _, stdout, stderr = execute.execute_and_filter_output( ++ [ ++ "ditto", ++ f"{developer_dir}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/iphoneos/libswift_Concurrency.dylib", ++ os.path.join(destination_path, "libswift_Concurrency.dylib") ++ ], raise_on_failure=True) ++ if stderr: ++ print(stderr) ++ if stdout: ++ print(stdout) ++ + + def _lipo_exec_files(exec_files, target_archs, strip_bitcode, source_path, + destination_path): diff --git a/Swiftgram/Playground/.swiftformat b/Swiftgram/Playground/.swiftformat new file mode 100644 index 00000000000..842cb77a795 --- /dev/null +++ b/Swiftgram/Playground/.swiftformat @@ -0,0 +1,3 @@ +--maxwidth 100 +--indent 4 +--disable redundantSelf \ No newline at end of file diff --git a/Swiftgram/Playground/BUILD b/Swiftgram/Playground/BUILD new file mode 100644 index 00000000000..f860378633e --- /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 00000000000..221a308b193 --- /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 00000000000..95fdf06b7de --- /dev/null +++ b/Swiftgram/Playground/Resources/Info.plist @@ -0,0 +1,39 @@ + + + + + UILaunchScreen + + UILaunchScreen + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + \ No newline at end of file diff --git a/Swiftgram/Playground/Resources/LaunchScreen.storyboard b/Swiftgram/Playground/Resources/LaunchScreen.storyboard new file mode 100644 index 00000000000..865e9329f37 --- /dev/null +++ b/Swiftgram/Playground/Resources/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swiftgram/Playground/Sources/AppDelegate.swift b/Swiftgram/Playground/Sources/AppDelegate.swift new file mode 100644 index 00000000000..69404da227b --- /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 00000000000..28b7549d450 --- /dev/null +++ b/Swiftgram/Playground/Sources/AppNavigationSetup.swift @@ -0,0 +1,100 @@ +import UIKit +import SwiftUI +import AsyncDisplayKit +import Display + +public func isKeyboardWindow(window: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: window)) + if #available(iOS 9.0, *) { + if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") { + return true + } + } else { + if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") { + return true + } + } + return false +} + +public func isKeyboardView(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") { + return true + } + return false +} + +public func isKeyboardViewContainer(view: NSObject) -> Bool { + let typeName = NSStringFromClass(type(of: view)) + if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") { + return true + } + return false +} + +public class ApplicationStatusBarHost: StatusBarHost { + private let application = UIApplication.shared + + public var isApplicationInForeground: Bool { + switch self.application.applicationState { + case .background: + return false + default: + return true + } + } + + public var statusBarFrame: CGRect { + return self.application.statusBarFrame + } + public var statusBarStyle: UIStatusBarStyle { + get { + return self.application.statusBarStyle + } set(value) { + self.setStatusBarStyle(value, animated: false) + } + } + + public func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) { + if self.shouldChangeStatusBarStyle?(style) ?? true { + self.application.internalSetStatusBarStyle(style, animated: animated) + } + } + + public var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)? + + public func setStatusBarHidden(_ value: Bool, animated: Bool) { + self.application.internalSetStatusBarHidden(value, animation: animated ? .fade : .none) + } + + public var keyboardWindow: UIWindow? { + if #available(iOS 16.0, *) { + return UIApplication.shared.internalGetKeyboard() + } + + for window in UIApplication.shared.windows { + if isKeyboardWindow(window: window) { + return window + } + } + return nil + } + + public var keyboardView: UIView? { + guard let keyboardWindow = self.keyboardWindow else { + return nil + } + + for view in keyboardWindow.subviews { + if isKeyboardViewContainer(view: view) { + for subview in view.subviews { + if isKeyboardView(view: subview) { + return subview + } + } + } + } + return nil + } +} diff --git a/Swiftgram/Playground/Sources/Application.swift b/Swiftgram/Playground/Sources/Application.swift new file mode 100644 index 00000000000..12e8255877d --- /dev/null +++ b/Swiftgram/Playground/Sources/Application.swift @@ -0,0 +1,5 @@ +import UIKit + +@objc(Application) class Application: UIApplication { + +} \ No newline at end of file diff --git a/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift b/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift new file mode 100644 index 00000000000..982fcbf4798 --- /dev/null +++ b/Swiftgram/Playground/Sources/Example/PlaygroundSplashScreen.swift @@ -0,0 +1,95 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private final class PlaygroundSplashScreenNode: ASDisplayNode { + private let headerBackgroundNode: ASDisplayNode + private let headerCornerNode: ASImageNode + + private var isDismissed = false + + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + override init() { + self.headerBackgroundNode = ASDisplayNode() + self.headerBackgroundNode.backgroundColor = .black + + self.headerCornerNode = ASImageNode() + self.headerCornerNode.displaysAsynchronously = false + self.headerCornerNode.displayWithoutProcessing = true + self.headerCornerNode.image = generateImage(CGSize(width: 20.0, height: 10.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 20.0, height: 20.0))) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1) + + super.init() + + self.backgroundColor = THEME.list.itemBlocksBackgroundColor + + self.addSubnode(self.headerBackgroundNode) + self.addSubnode(self.headerCornerNode) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + if self.isDismissed { + return + } + self.validLayout = (layout, navigationHeight) + + let headerHeight = navigationHeight + 260.0 + + transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: 0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight))) + transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: 10.0))) + } + + func animateOut(completion: @escaping () -> Void) { + guard let (layout, navigationHeight) = self.validLayout else { + completion() + return + } + self.isDismissed = true + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + let headerHeight = navigationHeight + 260.0 + + transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: -headerHeight - 10.0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight)), completion: { _ in + completion() + }) + transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: CGSize(width: layout.size.width, height: 10.0))) + } +} + +public final class PlaygroundSplashScreen: ViewController { + + public init() { + + let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: true, separatorColor: .clear, badgeBackgroundColor: THEME.navigationBar.badgeBackgroundColor, badgeStrokeColor: THEME.navigationBar.badgeStrokeColor, badgeTextColor: THEME.navigationBar.badgeTextColor) + + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: "", close: ""))) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PlaygroundSplashScreenNode() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! PlaygroundSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + public func animateOut(completion: @escaping () -> Void) { + self.statusBar.statusBarStyle = .Black + (self.displayNode as! PlaygroundSplashScreenNode).animateOut(completion: completion) + } +} diff --git a/Swiftgram/Playground/Sources/PlaygroundTheme.swift b/Swiftgram/Playground/Sources/PlaygroundTheme.swift new file mode 100644 index 00000000000..b05d7933461 --- /dev/null +++ b/Swiftgram/Playground/Sources/PlaygroundTheme.swift @@ -0,0 +1,362 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit + + +public final class PlaygroundInfoTheme { + public let buttonBackgroundColor: UIColor + public let buttonTextColor: UIColor + public let incomingFundsTitleColor: UIColor + public let outgoingFundsTitleColor: UIColor + + public init( + buttonBackgroundColor: UIColor, + buttonTextColor: UIColor, + incomingFundsTitleColor: UIColor, + outgoingFundsTitleColor: UIColor + ) { + self.buttonBackgroundColor = buttonBackgroundColor + self.buttonTextColor = buttonTextColor + self.incomingFundsTitleColor = incomingFundsTitleColor + self.outgoingFundsTitleColor = outgoingFundsTitleColor + } +} + +public final class PlaygroundTransactionTheme { + public let descriptionBackgroundColor: UIColor + public let descriptionTextColor: UIColor + + public init( + descriptionBackgroundColor: UIColor, + descriptionTextColor: UIColor + ) { + self.descriptionBackgroundColor = descriptionBackgroundColor + self.descriptionTextColor = descriptionTextColor + } +} + +public final class PlaygroundSetupTheme { + public let buttonFillColor: UIColor + public let buttonForegroundColor: UIColor + public let inputBackgroundColor: UIColor + public let inputPlaceholderColor: UIColor + public let inputTextColor: UIColor + public let inputClearButtonColor: UIColor + + public init( + buttonFillColor: UIColor, + buttonForegroundColor: UIColor, + inputBackgroundColor: UIColor, + inputPlaceholderColor: UIColor, + inputTextColor: UIColor, + inputClearButtonColor: UIColor + ) { + self.buttonFillColor = buttonFillColor + self.buttonForegroundColor = buttonForegroundColor + self.inputBackgroundColor = inputBackgroundColor + self.inputPlaceholderColor = inputPlaceholderColor + self.inputTextColor = inputTextColor + self.inputClearButtonColor = inputClearButtonColor + } +} + +public final class PlaygroundListTheme { + public let itemPrimaryTextColor: UIColor + public let itemSecondaryTextColor: UIColor + public let itemPlaceholderTextColor: UIColor + public let itemDestructiveColor: UIColor + public let itemAccentColor: UIColor + public let itemDisabledTextColor: UIColor + public let plainBackgroundColor: UIColor + public let blocksBackgroundColor: UIColor + public let itemPlainSeparatorColor: UIColor + public let itemBlocksBackgroundColor: UIColor + public let itemBlocksSeparatorColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let sectionHeaderTextColor: UIColor + public let freeTextColor: UIColor + public let freeTextErrorColor: UIColor + public let inputClearButtonColor: UIColor + + public init( + itemPrimaryTextColor: UIColor, + itemSecondaryTextColor: UIColor, + itemPlaceholderTextColor: UIColor, + itemDestructiveColor: UIColor, + itemAccentColor: UIColor, + itemDisabledTextColor: UIColor, + plainBackgroundColor: UIColor, + blocksBackgroundColor: UIColor, + itemPlainSeparatorColor: UIColor, + itemBlocksBackgroundColor: UIColor, + itemBlocksSeparatorColor: UIColor, + itemHighlightedBackgroundColor: UIColor, + sectionHeaderTextColor: UIColor, + freeTextColor: UIColor, + freeTextErrorColor: UIColor, + inputClearButtonColor: UIColor + ) { + self.itemPrimaryTextColor = itemPrimaryTextColor + self.itemSecondaryTextColor = itemSecondaryTextColor + self.itemPlaceholderTextColor = itemPlaceholderTextColor + self.itemDestructiveColor = itemDestructiveColor + self.itemAccentColor = itemAccentColor + self.itemDisabledTextColor = itemDisabledTextColor + self.plainBackgroundColor = plainBackgroundColor + self.blocksBackgroundColor = blocksBackgroundColor + self.itemPlainSeparatorColor = itemPlainSeparatorColor + self.itemBlocksBackgroundColor = itemBlocksBackgroundColor + self.itemBlocksSeparatorColor = itemBlocksSeparatorColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.sectionHeaderTextColor = sectionHeaderTextColor + self.freeTextColor = freeTextColor + self.freeTextErrorColor = freeTextErrorColor + self.inputClearButtonColor = inputClearButtonColor + } +} + +public final class PlaygroundTheme: Equatable { + public let info: PlaygroundInfoTheme + public let transaction: PlaygroundTransactionTheme + public let setup: PlaygroundSetupTheme + public let list: PlaygroundListTheme + public let statusBarStyle: StatusBarStyle + public let navigationBar: NavigationBarTheme + public let keyboardAppearance: UIKeyboardAppearance + public let alert: AlertControllerTheme + public let actionSheet: ActionSheetControllerTheme + + private let resourceCache = PlaygroundThemeResourceCache() + + public init(info: PlaygroundInfoTheme, transaction: PlaygroundTransactionTheme, setup: PlaygroundSetupTheme, list: PlaygroundListTheme, statusBarStyle: StatusBarStyle, navigationBar: NavigationBarTheme, keyboardAppearance: UIKeyboardAppearance, alert: AlertControllerTheme, actionSheet: ActionSheetControllerTheme) { + self.info = info + self.transaction = transaction + self.setup = setup + self.list = list + self.statusBarStyle = statusBarStyle + self.navigationBar = navigationBar + self.keyboardAppearance = keyboardAppearance + self.alert = alert + self.actionSheet = actionSheet + } + + func image(_ key: Int32, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? { + return self.resourceCache.image(key, self, generate) + } + + public static func ==(lhs: PlaygroundTheme, rhs: PlaygroundTheme) -> Bool { + return lhs === rhs + } +} + + +private final class PlaygroundThemeResourceCacheHolder { + var images: [Int32: UIImage] = [:] +} + +private final class PlaygroundThemeResourceCache { + private let imageCache = Atomic(value: PlaygroundThemeResourceCacheHolder()) + + public func image(_ key: Int32, _ theme: PlaygroundTheme, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? { + let result = self.imageCache.with { holder -> UIImage? in + return holder.images[key] + } + if let result = result { + return result + } else { + if let image = generate(theme) { + self.imageCache.with { holder -> Void in + holder.images[key] = image + } + return image + } else { + return nil + } + } + } +} + +enum PlaygroundThemeResourceKey: Int32 { + case itemListCornersBoth + case itemListCornersTop + case itemListCornersBottom + case itemListClearInputIcon + case itemListDisclosureArrow + case navigationShareIcon + case transactionLockIcon + + case clockMin + case clockFrame +} + +func cornersImage(_ theme: PlaygroundTheme, top: Bool, bottom: Bool) -> UIImage? { + if !top && !bottom { + return nil + } + let key: PlaygroundThemeResourceKey + if top && bottom { + key = .itemListCornersBoth + } else if top { + key = .itemListCornersTop + } else { + key = .itemListCornersBottom + } + return theme.image(key.rawValue, { theme in + return generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.setFillColor(theme.list.blocksBackgroundColor.cgColor) + context.fill(bounds) + + context.setBlendMode(.clear) + + var corners: UIRectCorner = [] + if top { + corners.insert(.topLeft) + corners.insert(.topRight) + } + if bottom { + corners.insert(.bottomLeft) + corners.insert(.bottomRight) + } + let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0)) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) + }) +} + +func itemListClearInputIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.itemListClearInputIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/ClearInput"), color: theme.list.inputClearButtonColor) + }) +} + +func navigationShareIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.navigationShareIcon.rawValue, { theme in + generateTintedImage(image: UIImage(bundleImageName: "Playground/NavigationShare"), color: theme.navigationBar.buttonColor) + }) +} + +func disclosureArrowImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.itemListDisclosureArrow.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/DisclosureArrow"), color: theme.list.itemSecondaryTextColor) + }) +} + +func clockFrameImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.clockFrame.rawValue, { theme in + let color = theme.list.itemSecondaryTextColor + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth)) + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0)) + }) + }) +} + +func clockMinImage(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.clockMin.rawValue, { theme in + let color = theme.list.itemSecondaryTextColor + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth)) + }) + }) +} + +func PlaygroundTransactionLockIcon(_ theme: PlaygroundTheme) -> UIImage? { + return theme.image(PlaygroundThemeResourceKey.transactionLockIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Playground/EncryptedComment"), color: theme.list.itemSecondaryTextColor) + }) +} + + +public let ACCENT_COLOR = UIColor(rgb: 0x007ee5) +public let NAVIGATION_BAR_THEME = NavigationBarTheme( + buttonColor: ACCENT_COLOR, + disabledButtonColor: UIColor(rgb: 0xd0d0d0), + primaryTextColor: .black, + backgroundColor: UIColor(rgb: 0xf7f7f7), + enableBackgroundBlur: true, + separatorColor: UIColor(rgb: 0xb1b1b1), + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeStrokeColor: UIColor(rgb: 0xff3b30), + badgeTextColor: .white +) +public let THEME = PlaygroundTheme( + info: PlaygroundInfoTheme( + buttonBackgroundColor: UIColor(rgb: 0x32aafe), + buttonTextColor: .white, + incomingFundsTitleColor: UIColor(rgb: 0x00b12c), + outgoingFundsTitleColor: UIColor(rgb: 0xff3b30) + ), transaction: PlaygroundTransactionTheme( + descriptionBackgroundColor: UIColor(rgb: 0xf1f1f4), + descriptionTextColor: .black + ), setup: PlaygroundSetupTheme( + buttonFillColor: ACCENT_COLOR, + buttonForegroundColor: .white, + inputBackgroundColor: UIColor(rgb: 0xe9e9e9), + inputPlaceholderColor: UIColor(rgb: 0x818086), + inputTextColor: .black, + inputClearButtonColor: UIColor(rgb: 0x7b7b81).withAlphaComponent(0.8) + ), + list: PlaygroundListTheme( + itemPrimaryTextColor: .black, + itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), + itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), + itemDestructiveColor: UIColor(rgb: 0xff3b30), + itemAccentColor: ACCENT_COLOR, + itemDisabledTextColor: UIColor(rgb: 0x8e8e93), + plainBackgroundColor: .white, + blocksBackgroundColor: UIColor(rgb: 0xefeff4), + itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemBlocksBackgroundColor: .white, + itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea), + sectionHeaderTextColor: UIColor(rgb: 0x6d6d72), + freeTextColor: UIColor(rgb: 0x6d6d72), + freeTextErrorColor: UIColor(rgb: 0xcf3030), + inputClearButtonColor: UIColor(rgb: 0xcccccc) + ), + statusBarStyle: .Black, + navigationBar: NAVIGATION_BAR_THEME, + keyboardAppearance: .light, + alert: AlertControllerTheme( + backgroundType: .light, + backgroundColor: .white, + separatorColor: UIColor(white: 0.9, alpha: 1.0), + highlightedItemColor: UIColor(rgb: 0xe5e5ea), + primaryColor: .black, + secondaryColor: UIColor(rgb: 0x5e5e5e), + accentColor: ACCENT_COLOR, + contrastColor: .green, + destructiveColor: UIColor(rgb: 0xff3b30), + disabledColor: UIColor(rgb: 0xd0d0d0), + controlBorderColor: .green, + baseFontSize: 17.0 + ), + actionSheet: ActionSheetControllerTheme( + dimColor: UIColor(white: 0.0, alpha: 0.4), + backgroundType: .light, + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0), + standardActionTextColor: ACCENT_COLOR, + destructiveActionTextColor: UIColor(rgb: 0xff3b30), + disabledActionTextColor: UIColor(rgb: 0xb3b3b3), + primaryTextColor: .black, + secondaryTextColor: UIColor(rgb: 0x5e5e5e), + controlAccentColor: ACCENT_COLOR, + controlColor: UIColor(rgb: 0x7e8791), + switchFrameColor: UIColor(rgb: 0xe0e0e0), + switchContentColor: UIColor(rgb: 0x77d572), + switchHandleColor: UIColor(rgb: 0xffffff), + baseFontSize: 17.0 + ) +) diff --git a/Swiftgram/Playground/Sources/SwiftUIViewController.swift b/Swiftgram/Playground/Sources/SwiftUIViewController.swift new file mode 100644 index 00000000000..139230a38a6 --- /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 00000000000..a63f787ddab --- /dev/null +++ b/Swiftgram/Playground/Sources/main.m @@ -0,0 +1,7 @@ +#import + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, @"Application", @"AppDelegate"); + } +} \ No newline at end of file diff --git a/Swiftgram/Playground/generate_project.py b/Swiftgram/Playground/generate_project.py new file mode 100755 index 00000000000..cc2e135174e --- /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 00000000000..feefa4f941f --- /dev/null +++ b/Swiftgram/Playground/launch_on_simulator.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +import subprocess +import json +import os +import time + + +def find_app(start_path): + for root, dirs, _ in os.walk(start_path): + for dir in dirs: + if dir.endswith(".app"): + return os.path.join(root, dir) + return None + + +def ensure_simulator_booted(device_name) -> str: + # List all devices + devices_json = subprocess.check_output( + ["xcrun", "simctl", "list", "devices", "--json"] + ).decode() + devices = json.loads(devices_json) + for runtime in devices["devices"]: + for device in devices["devices"][runtime]: + if device["name"] == device_name: + device_udid = device["udid"] + if device["state"] == "Booted": + print(f"Simulator {device_name} is already booted.") + return device_udid + break + if device_udid: + break + + if not device_udid: + raise Exception(f"Simulator {device_name} not found") + + # Boot the device + print(f"Booting simulator {device_name}...") + subprocess.run(["xcrun", "simctl", "boot", device_udid], check=True) + + # Wait for the device to finish booting + print("Waiting for simulator to finish booting...") + while True: + boot_status = subprocess.check_output( + ["xcrun", "simctl", "list", "devices"] + ).decode() + if f"{device_name} ({device_udid}) (Booted)" in boot_status: + break + time.sleep(0.5) + + print(f"Simulator {device_name} is now booted.") + return device_udid + + +def build_and_run_xcode_project(project_path, scheme_name, destination): + # Change to the directory containing the .xcodeproj file + os.chdir(os.path.dirname(project_path)) + + # Build the project + build_command = [ + "xcodebuild", + "-project", + project_path, + "-scheme", + scheme_name, + "-destination", + destination, + "-sdk", + "iphonesimulator", + "build", + ] + + try: + subprocess.run(build_command, check=True) + print("Build successful!") + except subprocess.CalledProcessError as e: + print(f"Build failed with error: {e}") + return + + # Get the bundle identifier and app path + settings_command = [ + "xcodebuild", + "-project", + project_path, + "-scheme", + scheme_name, + "-sdk", + "iphonesimulator", + "-showBuildSettings", + ] + + try: + result = subprocess.run( + settings_command, capture_output=True, text=True, check=True + ) + settings = result.stdout.split("\n") + bundle_id = next( + line.split("=")[1].strip() + for line in settings + if "PRODUCT_BUNDLE_IDENTIFIER" in line + ) + build_dir = next( + line.split("=")[1].strip() + for line in settings + if "TARGET_BUILD_DIR" in line + ) + + app_path = find_app(build_dir) + if not app_path: + print(f"Could not find .app file in {build_dir}") + return + print(f"Found app at: {app_path}") + print(f"Bundle identifier: {bundle_id}") + print(f"App path: {app_path}") + except (subprocess.CalledProcessError, StopIteration) as e: + print(f"Failed to get build settings: {e}") + return + + device_udid = ensure_simulator_booted(simulator_name) + + # Install the app on the simulator + install_command = ["xcrun", "simctl", "install", device_udid, app_path] + + try: + subprocess.run(install_command, check=True) + print("App installed on simulator successfully!") + except subprocess.CalledProcessError as e: + print(f"Failed to install app on simulator: {e}") + return + + # List installed apps + try: + listapps_cmd = "/usr/bin/xcrun simctl listapps booted | /usr/bin/plutil -convert json -r -o - -- -" + result = subprocess.run( + listapps_cmd, shell=True, capture_output=True, text=True, check=True + ) + apps = json.loads(result.stdout) + + if bundle_id in apps: + print(f"App {bundle_id} is installed on the simulator") + else: + print(f"App {bundle_id} is not installed on the simulator") + print("Installed apps:", list(apps.keys())) + except subprocess.CalledProcessError as e: + print(f"Failed to list apps: {e}") + except json.JSONDecodeError as e: + print(f"Failed to parse app list: {e}") + + # Focus simulator + subprocess.run(["open", "-a", "Simulator"], check=True) + + # Run the project on the simulator + run_command = ["xcrun", "simctl", "launch", "booted", bundle_id] + + try: + subprocess.run(run_command, check=True) + print("Application launched in simulator!") + except subprocess.CalledProcessError as e: + print(f"Failed to launch application in simulator: {e}") + + +# Usage +current_script_dir = os.path.dirname(os.path.abspath(__file__)) +project_path = os.path.join(current_script_dir, "Playground.xcodeproj") +scheme_name = "Playground" +simulator_name = "iPhone 15" +destination = f"platform=iOS Simulator,name={simulator_name},OS=latest" + +if __name__ == "__main__": + build_and_run_xcode_project(project_path, scheme_name, destination) diff --git a/Swiftgram/SFSafariViewControllerPlus/BUILD b/Swiftgram/SFSafariViewControllerPlus/BUILD new file mode 100644 index 00000000000..72a719f0b1e --- /dev/null +++ b/Swiftgram/SFSafariViewControllerPlus/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SFSafariViewControllerPlus", + module_name = "SFSafariViewControllerPlus", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift b/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift new file mode 100644 index 00000000000..1df3ddbaa33 --- /dev/null +++ b/Swiftgram/SFSafariViewControllerPlus/Sources/SFSafariViewControllerPlus.swift @@ -0,0 +1,14 @@ +import SafariServices + +public class SFSafariViewControllerPlusDidFinish: SFSafariViewController, SFSafariViewControllerDelegate { + public var onDidFinish: (() -> Void)? + + public override init(url URL: URL, configuration: SFSafariViewController.Configuration = SFSafariViewController.Configuration()) { + super.init(url: URL, configuration: configuration) + self.delegate = self + } + + public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + onDidFinish?() + } +} diff --git a/Swiftgram/SGAPI/BUILD b/Swiftgram/SGAPI/BUILD new file mode 100644 index 00000000000..1a7634e2c8c --- /dev/null +++ b/Swiftgram/SGAPI/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPI", + module_name = "SGAPI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGWebAppExtensions:SGWebAppExtensions", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme", + "//Swiftgram/SGRegDateScheme:SGRegDateScheme", + "//Swiftgram/SGRequests:SGRequests", + "//Swiftgram/SGConfig:SGConfig" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPI/Sources/SGAPI.swift b/Swiftgram/SGAPI/Sources/SGAPI.swift new file mode 100644 index 00000000000..9a85f8093cb --- /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 00000000000..9b507e1c2bf --- /dev/null +++ b/Swiftgram/SGAPIToken/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPIToken", + module_name = "SGAPIToken", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramCore:TelegramCore", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme", + "//Swiftgram/SGConfig:SGConfig", + "//Swiftgram/SGWebAppExtensions:SGWebAppExtensions", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPIToken/Sources/SGAPIToken.swift b/Swiftgram/SGAPIToken/Sources/SGAPIToken.swift new file mode 100644 index 00000000000..209cbb04715 --- /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 00000000000..9964398d276 --- /dev/null +++ b/Swiftgram/SGAPIWebSettings/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGAPIWebSettings", + module_name = "SGAPIWebSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramCore:TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGAPIWebSettings/Sources/File.swift b/Swiftgram/SGAPIWebSettings/Sources/File.swift new file mode 100644 index 00000000000..bfb022afaa0 --- /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 00000000000..a27377792c7 --- /dev/null +++ b/Swiftgram/SGActionRequestHandlerSanitizer/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGActionRequestHandlerSanitizer", + module_name = "SGActionRequestHandlerSanitizer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift b/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift new file mode 100644 index 00000000000..f94edc1c686 --- /dev/null +++ b/Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift @@ -0,0 +1,15 @@ +import Foundation + +public func sgActionRequestHandlerSanitizer(_ url: URL) -> URL { + var url = url + if let scheme = url.scheme { + let openInPrefix = "\(scheme)://parseurl?url=" + let urlString = url.absoluteString + if urlString.hasPrefix(openInPrefix) { + if let unwrappedUrlString = String(urlString.dropFirst(openInPrefix.count)).removingPercentEncoding, let newUrl = URL(string: unwrappedUrlString) { + url = newUrl + } + } + } + return url +} diff --git a/Swiftgram/SGAppBadgeAssets/BUILD b/Swiftgram/SGAppBadgeAssets/BUILD new file mode 100644 index 00000000000..d916184c297 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/BUILD @@ -0,0 +1,5 @@ +filegroup( + name = "SGAppBadgeAssets", + srcs = glob(["Images.xcassets/**"]), + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..b1a95561162 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Day@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Day@3x.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Day@3x.png new file mode 100644 index 00000000000..d335dc3d848 Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DayAppBadge.imageset/Day@3x.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..3e95b39a562 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Ducky@3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Ducky@3.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Ducky@3.png new file mode 100644 index 00000000000..69b26db1170 Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/DuckyAppBadge.imageset/Ducky@3.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..baa6cad8796 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Night@3-1.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Night@3-1.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Night@3-1.png new file mode 100644 index 00000000000..b4013589d83 Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/NightAppBadge.imageset/Night@3-1.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..7007dd0f052 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Pro@3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Pro@3.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Pro@3.png new file mode 100644 index 00000000000..aba68aaac8e Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/ProAppBadge.imageset/Pro@3.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..e6afa8ae5bb --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Sky@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Sky@3x.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Sky@3x.png new file mode 100644 index 00000000000..4b2894c850e Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SkyAppBadge.imageset/Sky@3x.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..4b7eedb9f75 --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Sparkling@3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Sparkling@3.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Sparkling@3.png new file mode 100644 index 00000000000..829e3624cf0 Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/SparklingAppBadge.imageset/Sparkling@3.png differ diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Contents.json b/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Contents.json new file mode 100644 index 00000000000..c3b56ecd5fd --- /dev/null +++ b/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Titanium@3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Titanium@3.png b/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Titanium@3.png new file mode 100644 index 00000000000..2dc023448d1 Binary files /dev/null and b/Swiftgram/SGAppBadgeAssets/Images.xcassets/TitaniumAppBadge.imageset/Titanium@3.png differ diff --git a/Swiftgram/SGAppBadgeOffset/BUILD b/Swiftgram/SGAppBadgeOffset/BUILD new file mode 100644 index 00000000000..23409546fb6 --- /dev/null +++ b/Swiftgram/SGAppBadgeOffset/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGAppBadgeOffset", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGAppBadgeOffset/Sources/SGAppBadgeOffset.swift b/Swiftgram/SGAppBadgeOffset/Sources/SGAppBadgeOffset.swift new file mode 100644 index 00000000000..06b0c89079b --- /dev/null +++ b/Swiftgram/SGAppBadgeOffset/Sources/SGAppBadgeOffset.swift @@ -0,0 +1,84 @@ +import UIKit +import DeviceModel + + +let DEVICE_MODELS_WITH_APP_BADGE_SUPPORT: [DeviceModel] = [ + .iPhoneX, + .iPhoneXS, + .iPhoneXSMax, + .iPhoneXR, + .iPhone11, + .iPhone11Pro, + .iPhone11ProMax, + .iPhone12, + .iPhone12Mini, + .iPhone12Pro, + .iPhone12ProMax, + .iPhone13, + .iPhone13Mini, + .iPhone13Pro, + .iPhone13ProMax, + .iPhone14, + .iPhone14Plus, + .iPhone14Pro, + .iPhone14ProMax, + .iPhone15, + .iPhone15Plus, + .iPhone15Pro, + .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, + .iPhone16Pro, + .iPhone16ProMax, + .iPhone16e +] + +extension DeviceMetrics { + + func sgAppBadgeOffset() -> CGFloat { + let currentDevice = DeviceModel.current + var defaultOffset: CGFloat = 0.0 + // https://www.ios-resolution.com/ + // Similar height + Scale + switch currentDevice { + case .iPhoneX, .iPhoneXS, .iPhone11Pro, .iPhone12Mini, .iPhone13Mini: + defaultOffset = 2.0 + case .iPhone11, .iPhoneXR: + defaultOffset = 6.0 + case .iPhone11ProMax, .iPhoneXSMax: + defaultOffset = 4.0 + case .iPhone12, .iPhone12Pro, .iPhone13, .iPhone13Pro, .iPhone14, .iPhone16e: + defaultOffset = 4.0 + case .iPhone12ProMax, .iPhone13ProMax, .iPhone14Plus: + defaultOffset = 6.0 + case .iPhone14Pro, .iPhone15, .iPhone15Pro, .iPhone16: + defaultOffset = 18.0 + case .iPhone14ProMax, .iPhone15Plus, .iPhone15ProMax, .iPhone16Plus: + defaultOffset = 19.0 + case .iPhone16Pro: + defaultOffset = 21.0 + case .iPhone16ProMax: + defaultOffset = 22.0 + default: + defaultOffset = 0.0 // Any device in 2025+ should be like iPhone 14 Pro or better + } + let offset: CGFloat = floorToScreenPixels(defaultOffset * self.sgScaleFactor) + #if DEBUG + print("deviceMetrics \(self). deviceModel: \(currentDevice). sgIsDisplayZoomed: \(self.sgIsDisplayZoomed). sgScaleFactor: \(self.sgScaleFactor) defaultOffset: \(defaultOffset), offset: \(offset)") + #endif + return offset + } + + var sgIsDisplayZoomed: Bool { + UIScreen.main.scale < UIScreen.main.nativeScale + } + + var sgScaleFactor: CGFloat { + UIScreen.main.scale / UIScreen.main.nativeScale + } + + var sgShowAppBadge: Bool { + return DEVICE_MODELS_WITH_APP_BADGE_SUPPORT.contains(DeviceModel.current) // MARK: Swiftgram + } + +} diff --git a/Swiftgram/SGAppGroupIdentifier/BUILD b/Swiftgram/SGAppGroupIdentifier/BUILD new file mode 100644 index 00000000000..cc3e13985c6 --- /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 00000000000..bf27bae7540 --- /dev/null +++ b/Swiftgram/SGAppGroupIdentifier/Sources/SGAppGroupIdentifier.swift @@ -0,0 +1,28 @@ +import Foundation + +public let FALLBACK_BASE_BUNDLE_ID: 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 00000000000..8679395f707 --- /dev/null +++ b/Swiftgram/SGContentAnalysis/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGContentAnalysis", + module_name = "SGContentAnalysis", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift b/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift new file mode 100644 index 00000000000..b75ba3fd3e4 --- /dev/null +++ b/Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift @@ -0,0 +1,64 @@ +import SensitiveContentAnalysis +import SwiftSignalKit + +public enum ContentAnalysisError: Error { + case generic(_ message: String) +} + +public enum ContentAnalysisMediaType { + case image + case video +} + +public func canAnalyzeMedia() -> Bool { + if #available(iOS 17, *) { + let analyzer = SCSensitivityAnalyzer() + let policy = analyzer.analysisPolicy + return policy != .disabled + } else { + return false + } +} + + +public func analyzeMediaSignal(_ url: URL, mediaType: ContentAnalysisMediaType = .image) -> Signal { + return Signal { subscriber in + analyzeMedia(url: url, mediaType: mediaType, completion: { result, error in + if let result = result { + subscriber.putNext(result) + subscriber.putCompletion() + } else if let error = error { + subscriber.putError(error) + } else { + subscriber.putError(ContentAnalysisError.generic("Unknown response")) + } + }) + + return ActionDisposable { + } + } +} + +private func analyzeMedia(url: URL, mediaType: ContentAnalysisMediaType, completion: @escaping (Bool?, Error?) -> Void) { + if #available(iOS 17, *) { + let analyzer = SCSensitivityAnalyzer() + switch mediaType { + case .image: + analyzer.analyzeImage(at: url) { analysisResult, analysisError in + completion(analysisResult?.isSensitive, analysisError) + } + case .video: + Task { + do { + let handler = analyzer.videoAnalysis(forFileAt: url) + let response = try await handler.hasSensitiveContent() + completion(response.isSensitive, nil) + } catch { + completion(nil, error) + } + } + } + } else { + completion(false, nil) + } +} diff --git a/Swiftgram/SGDBReset/BUILD b/Swiftgram/SGDBReset/BUILD new file mode 100644 index 00000000000..c9e2113bd6f --- /dev/null +++ b/Swiftgram/SGDBReset/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGDBReset", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGDBReset/Sources/File.swift b/Swiftgram/SGDBReset/Sources/File.swift new file mode 100644 index 00000000000..3cb9b939521 --- /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 00000000000..c3a6130f24a --- /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 00000000000..b0ea9d28ae6 --- /dev/null +++ b/Swiftgram/SGDebugUI/Sources/SGDebugUI.swift @@ -0,0 +1,224 @@ +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 clearOutgoingTranslationLanguageCache + 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(.action(id: id.count, section: .base, actionType: .clearOutgoingTranslationLanguageCache, text: "Clear Outgoing Translation 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 .clearOutgoingTranslationLanguageCache: + SGLogger.shared.log("SGDebug", "Outgoing translation language cache cleanup init") + SGSimpleSettings.shared.outgoingLanguageTranslation.drop() + SGLogger.shared.log("SGDebug", "Outgoing translation language cache cleanup succesfull") + presentControllerImpl?(okUndoController("OK: Outgoing translation language cache cleaned", presentationData), nil) + 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 00000000000..8a1446f3f1f --- /dev/null +++ b/Swiftgram/SGDeviceToken/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGDeviceToken", + module_name = "SGDeviceToken", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGDeviceToken/Sources/File.swift b/Swiftgram/SGDeviceToken/Sources/File.swift new file mode 100644 index 00000000000..abf7df33570 --- /dev/null +++ b/Swiftgram/SGDeviceToken/Sources/File.swift @@ -0,0 +1,31 @@ +import SwiftSignalKit +import DeviceCheck + +public enum SGDeviceTokenError { + case unsupportedDevice + case generic(String) +} + +public func getDeviceToken() -> Signal { + return Signal { subscriber in + let currentDevice = DCDevice.current + if currentDevice.isSupported { + currentDevice.generateToken { (data, error) in + guard error == nil else { + subscriber.putError(.generic(error!.localizedDescription)) + return + } + if let tokenData = data { + subscriber.putNext(tokenData.base64EncodedString()) + subscriber.putCompletion() + } else { + subscriber.putError(.generic("Empty Token")) + } + } + } else { + subscriber.putError(.unsupportedDevice) + } + return ActionDisposable { + } + } +} diff --git a/Swiftgram/SGDoubleTapMessageAction/BUILD b/Swiftgram/SGDoubleTapMessageAction/BUILD new file mode 100644 index 00000000000..ac9be00d708 --- /dev/null +++ b/Swiftgram/SGDoubleTapMessageAction/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGDoubleTapMessageAction", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift b/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift new file mode 100644 index 00000000000..2cefa9b8473 --- /dev/null +++ b/Swiftgram/SGDoubleTapMessageAction/Sources/SGDoubleTapMessageAction.swift @@ -0,0 +1,13 @@ +import Foundation +import SGSimpleSettings +import Postbox +import TelegramCore + + +func sgDoubleTapMessageAction(incoming: Bool, message: Message) -> String { + if incoming { + return SGSimpleSettings.MessageDoubleTapAction.default.rawValue + } else { + return SGSimpleSettings.shared.messageDoubleTapActionOutgoing + } +} diff --git a/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD b/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD new file mode 100644 index 00000000000..87428676030 --- /dev/null +++ b/Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGEmojiKeyboardDefaultFirst", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift b/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift new file mode 100644 index 00000000000..8d582084e29 --- /dev/null +++ b/Swiftgram/SGEmojiKeyboardDefaultFirst/Sources/SGEmojiKeyboardDefaultFirst.swift @@ -0,0 +1,23 @@ +import Foundation + + +func sgPatchEmojiKeyboardItems(_ items: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] { + var items = items + let staticEmojisIndex = items.firstIndex { item in + if let groupId = item.groupId.base as? String, groupId == "static" { + return true + } + return false + } + let recentEmojisIndex = items.firstIndex { item in + if let groupId = item.groupId.base as? String, groupId == "recent" { + return true + } + return false + } + if let staticEmojisIndex = staticEmojisIndex { + let staticEmojiItem = items.remove(at: staticEmojisIndex) + items.insert(staticEmojiItem, at: (recentEmojisIndex ?? -1) + 1 ) + } + return items +} \ No newline at end of file diff --git a/Swiftgram/SGGHSettings/BUILD b/Swiftgram/SGGHSettings/BUILD new file mode 100644 index 00000000000..94ed6252386 --- /dev/null +++ b/Swiftgram/SGGHSettings/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGGHSettings", + module_name = "SGGHSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGGHSettingsScheme:SGGHSettingsScheme", + "//Swiftgram/SGLogging:SGLogging", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramCore:TelegramCore", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGGHSettings/Sources/SGGHSettings.swift b/Swiftgram/SGGHSettings/Sources/SGGHSettings.swift new file mode 100644 index 00000000000..750943d2527 --- /dev/null +++ b/Swiftgram/SGGHSettings/Sources/SGGHSettings.swift @@ -0,0 +1,99 @@ +import Foundation +import SGLogging +import SGGHSettingsScheme +import AccountContext +import TelegramCore + + +public func updateSGGHSettingsInteractivelly(context: AccountContext) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let locale = presentationData.strings.baseLanguageCode + let _ = Task { + do { + let settings = try await fetchSGGHSettings(locale: locale) + let _ = await (context.account.postbox.transaction { transaction in + updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in + var configuration = configuration + configuration.sgGHSettings = settings + return configuration + }) + }).task() + } catch { + return + } + + } +} + + +let maxRetries: Int = 3 + +enum SGGHFetchError: Error { + case invalidURL + case notFound + case fetchFailed(statusCode: Int) + case decodingFailed +} + +func fetchSGGHSettings(locale: String) async throws -> SGGHSettings { + let baseURL = "https://raw.githubusercontent.com/Swiftgram/settings/refs/heads/main" + var candidates: [String] = [] + if let buildNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + if locale != "en" { + candidates.append("\(buildNumber)_\(locale).json") + } + candidates.append("\(buildNumber).json") + } + if locale != "en" { + candidates.append("latest_\(locale).json") + } + candidates.append("latest.json") + + var lastError: Error? + for candidate in candidates { + let urlString = "\(baseURL)/\(candidate)" + guard let url = URL(string: urlString) else { + SGLogger.shared.log("SGGHSettings", "[0] Fetch failed for \(candidate). Invalid URL: \(urlString)") + continue + } + + attemptsOuter: for attempt in 1...maxRetries { + do { + let (data, response) = try await URLSession.shared.data(from: url) + guard let httpResponse = response as? HTTPURLResponse else { + SGLogger.shared.log("SGGHSettings", "[\(attempt)] Fetch failed for \(candidate). Invalid response type: \(response)") + throw SGGHFetchError.fetchFailed(statusCode: -1) + } + + switch httpResponse.statusCode { + case 200: + do { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + let settings = try jsonDecoder.decode(SGGHSettings.self, from: data) + SGLogger.shared.log("SGGHSettings", "[\(attempt)] Fetched \(candidate): \(settings)") + return settings + } catch { + SGLogger.shared.log("SGGHSettings", "[\(attempt)] Failed to decode \(candidate): \(error)") + throw SGGHFetchError.decodingFailed + } + case 404: + SGLogger.shared.log("SGGHSettings", "[\(attempt)] Not found \(candidate) on the remote.") + break attemptsOuter + default: + SGLogger.shared.log("SGGHSettings", "[\(attempt)] Fetch failed for \(candidate), status code: \(httpResponse.statusCode)") + throw SGGHFetchError.fetchFailed(statusCode: httpResponse.statusCode) + } + } catch { + lastError = error + if attempt == maxRetries { + break + } + try await Task.sleep(nanoseconds: UInt64(attempt * 2 * 1_000_000_000)) + } + } + } + + SGLogger.shared.log("SGGHSettings", "All attempts failed. Last error: \(String(describing: lastError))") + throw SGGHFetchError.fetchFailed(statusCode: -1) +} diff --git a/Swiftgram/SGGHSettingsScheme/BUILD b/Swiftgram/SGGHSettingsScheme/BUILD new file mode 100644 index 00000000000..105e0f6a8a5 --- /dev/null +++ b/Swiftgram/SGGHSettingsScheme/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGGHSettingsScheme", + module_name = "SGGHSettingsScheme", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGGHSettingsScheme/Sources/SGGHSettingsScheme.swift b/Swiftgram/SGGHSettingsScheme/Sources/SGGHSettingsScheme.swift new file mode 100644 index 00000000000..1335c375dea --- /dev/null +++ b/Swiftgram/SGGHSettingsScheme/Sources/SGGHSettingsScheme.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct SGGHSettings: Codable, Equatable { + public let announcementsData: String? + + public static var defaultValue: SGGHSettings { + return SGGHSettings(announcementsData: nil) + } +} \ No newline at end of file diff --git a/Swiftgram/SGIAP/BUILD b/Swiftgram/SGIAP/BUILD new file mode 100644 index 00000000000..c80d97254bc --- /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 00000000000..c2940161b47 --- /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 00000000000..99dbb60303d --- /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 00000000000..5063537930e --- /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 00000000000..6b2de974a1b --- /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 00000000000..a21f317e932 --- /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 00000000000..d0dd4589861 --- /dev/null +++ b/Swiftgram/SGItemListUI/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGItemListUI", + module_name = "SGItemListUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/MtProtoKit:MtProtoKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift b/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift new file mode 100644 index 00000000000..5a9d6d08522 --- /dev/null +++ b/Swiftgram/SGItemListUI/Sources/SGItemListUI.swift @@ -0,0 +1,335 @@ +// 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: { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) // Closing search keyboard if active + 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: { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) // Closing search keyboard if active + arguments.action(actionType) + }) + case let .searchInput(_, _, title, text, placeholder): + return ItemListSingleLineInputItem(presentationData: presentationData, title: title, text: text, placeholder: placeholder, returnKeyType: .done, spacing: 3.0, clearType: .always, selectAllOnFocus: true, secondaryStyle: true, sectionId: self.section, textUpdated: { input in arguments.searchInput(input) }, action: {}, dismissKeyboardOnEnter: true) + } + } +} + + +public func filterSGItemListUIEntrires( + entries: [SGItemListUIEntry], + by searchQuery: String? +) -> [SGItemListUIEntry] { + + guard let query = searchQuery?.lowercased(), !query.isEmpty else { + return entries + } + + var sectionIdsForEntireIncludion: Set = [] + var sectionIdsWithMatches: Set = [] + var filteredEntries: [SGItemListUIEntry] = [] + + func entryMatches(_ entry: SGItemListUIEntry, query: String) -> Bool { + switch entry { + case .header(_, _, let text, _): + return text.lowercased().contains(query) + case .toggle(_, _, _, _, let text, _): + return text.lowercased().contains(query) + case .notice(_, _, let text): + return text.lowercased().contains(query) + case .percentageSlider: + return false // Assuming percentage sliders don't have searchable text + case .oneFromManySelector(_, _, _, let text, let value, _): + return text.lowercased().contains(query) || value.lowercased().contains(query) + case .disclosure(_, _, _, let text): + return text.lowercased().contains(query) + case .peerColorDisclosurePreview: + return false // Never indexed during search + case .action(_, _, _, let text, _): + return text.lowercased().contains(query) + case .searchInput: + return true // Never hiding search input + } + } + + // First pass: identify sections with matches + for entry in entries { + if entryMatches(entry, query: query) { + switch entry { + case .searchInput: + continue + default: + sectionIdsWithMatches.insert(entry.section) + } + } + } + + // Second pass: keep matching entries and headers of sections with matches + for (index, entry) in entries.enumerated() { + switch entry { + case .header: + if entryMatches(entry, query: query) { + // Will show all entries for the same section + sectionIdsForEntireIncludion.insert(entry.section) + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + // Or show header if something from the section already matched + if sectionIdsWithMatches.contains(entry.section) { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + default: + if entryMatches(entry, query: query) { + if case .notice = entry { + // add previous entry to if it's not another notice and if it's not already here + // possibly targeting related toggle / setting if we've matched it's description (notice) in search + if index > 0 { + let previousEntry = entries[index - 1] + if case .notice = previousEntry {} else { + if !filteredEntries.contains(previousEntry) { + filteredEntries.append(previousEntry) + } + } + } + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } else { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + // add next entry if it's notice + // possibly targeting description (notice) for the currently search-matched toggle/setting + if index < entries.count - 1 { + let nextEntry = entries[index + 1] + if case .notice = nextEntry { + if !filteredEntries.contains(nextEntry) { + filteredEntries.append(nextEntry) + } + } + } + } + } else if sectionIdsForEntireIncludion.contains(entry.section) { + if !filteredEntries.contains(entry) { + filteredEntries.append(entry) + } + } + } + } + + return filteredEntries +} diff --git a/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift b/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift new file mode 100644 index 00000000000..ad61f47bba7 --- /dev/null +++ b/Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift @@ -0,0 +1,353 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import LegacyComponents +import ItemListUI +import PresentationDataUtils +import AppBundle + +public class SliderPercentageItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + public let sectionId: ItemListSectionId + let updated: (Int32) -> Void + + public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.value = value + self.sectionId = sectionId + self.updated = updated + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = SliderPercentageItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? SliderPercentageItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +private func rescalePercentageValueToSlider(_ value: CGFloat) -> CGFloat { + return max(0.0, min(1.0, value)) +} + +private func rescaleSliderValueToPercentageValue(_ value: CGFloat) -> CGFloat { + return max(0.0, min(1.0, value)) +} + +class SliderPercentageItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + private var sliderView: TGPhotoEditorSliderView? + private let leftTextNode: ImmediateTextNode + private let rightTextNode: ImmediateTextNode + private let centerTextNode: ImmediateTextNode + private let centerMeasureTextNode: ImmediateTextNode + + private let batteryImage: UIImage? + private let batteryBackgroundNode: ASImageNode + private let batteryForegroundNode: ASImageNode + + private var item: SliderPercentageItem? + private var layoutParams: ListViewItemLayoutParams? + + // MARK: Swiftgram + private let activateArea: AccessibilityAreaNode + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.leftTextNode = ImmediateTextNode() + self.rightTextNode = ImmediateTextNode() + self.centerTextNode = ImmediateTextNode() + self.centerMeasureTextNode = ImmediateTextNode() + + self.batteryImage = nil //UIImage(bundleImageName: "Settings/UsageBatteryFrame") + self.batteryBackgroundNode = ASImageNode() + self.batteryForegroundNode = ASImageNode() + + // MARK: Swiftgram + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.leftTextNode) + self.addSubnode(self.rightTextNode) + self.addSubnode(self.centerTextNode) + self.addSubnode(self.batteryBackgroundNode) + self.addSubnode(self.batteryForegroundNode) + self.addSubnode(self.activateArea) + + // MARK: Swiftgram + self.activateArea.increment = { [weak self] in + if let self { + self.sliderView?.increase(by: 0.10) + } + } + + self.activateArea.decrement = { [weak self] in + if let self { + self.sliderView?.decrease(by: 0.10) + } + } + } + + override func didLoad() { + super.didLoad() + + let sliderView = TGPhotoEditorSliderView() + sliderView.enableEdgeTap = true + sliderView.enablePanHandling = true + sliderView.trackCornerRadius = 1.0 + sliderView.lineSize = 4.0 + sliderView.minimumValue = 0.0 + sliderView.startValue = 0.0 + sliderView.maximumValue = 1.0 + sliderView.disablesInteractiveTransitionGestureRecognizer = true + sliderView.displayEdges = true + if let item = self.item, let params = self.layoutParams { + sliderView.value = rescalePercentageValueToSlider(CGFloat(item.value) / 100.0) + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSwitchColors.frameColor + sliderView.trackColor = item.theme.list.itemAccentColor + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) + } + self.view.addSubview(sliderView) + sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) + self.sliderView = sliderView + } + + func asyncLayout() -> (_ item: SliderPercentageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentItem = self.item + + return { item, params, neighbors in + var themeUpdated = false + if currentItem?.theme !== item.theme { + themeUpdated = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: params.width, height: 88.0) + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = params.leftInset + 16.0 + bottomStripeOffset = -separatorHeight + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + strongSelf.leftTextNode.attributedText = NSAttributedString(string: "0%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) + strongSelf.rightTextNode.attributedText = NSAttributedString(string: "100%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) + + let centralText: String = "\(item.value)%" + let centralMeasureText: String = centralText + strongSelf.batteryBackgroundNode.isHidden = true + strongSelf.batteryForegroundNode.isHidden = strongSelf.batteryBackgroundNode.isHidden + strongSelf.centerTextNode.attributedText = NSAttributedString(string: centralText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor) + strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centralMeasureText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor) + + strongSelf.leftTextNode.isAccessibilityElement = true + strongSelf.leftTextNode.accessibilityLabel = "Minimum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.minimumValue ?? 0.0) * 100.0))%" + strongSelf.rightTextNode.isAccessibilityElement = true + strongSelf.rightTextNode.accessibilityLabel = "Maximum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.maximumValue ?? 1.0) * 100.0))%" + + let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) + let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + + let sideInset: CGFloat = 18.0 + + strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize) + strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize) + + var centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0), size: centerTextSize) + if !strongSelf.batteryBackgroundNode.isHidden { + centerFrame.origin.x -= 12.0 + } + strongSelf.centerTextNode.frame = centerFrame + + if let frameImage = strongSelf.batteryImage { + strongSelf.batteryBackgroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: frameImage, color: item.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.9)) { + image.draw(in: CGRect(origin: CGPoint(), size: size)) + + let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0)) + context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath) + context.clip() + } + + UIGraphicsPopContext() + }) + strongSelf.batteryForegroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + + let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0)) + context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath) + context.clip() + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width * CGFloat(item.value) / 100.0, height: contentRect.height)), cornerRadius: 1.0).cgPath) + context.fillPath() + + UIGraphicsPopContext() + }) + + let batteryColor: UIColor + if item.value <= 20 { + batteryColor = UIColor(rgb: 0xFF3B30) + } else { + batteryColor = item.theme.list.itemSwitchColors.positiveColor + } + + if strongSelf.batteryForegroundNode.layer.layerTintColor == nil { + strongSelf.batteryForegroundNode.layer.layerTintColor = batteryColor.cgColor + } else { + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTintColor(layer: strongSelf.batteryForegroundNode.layer, color: batteryColor) + } + + strongSelf.batteryBackgroundNode.frame = CGRect(origin: CGPoint(x: centerFrame.minX + centerMeasureTextSize.width + 4.0, y: floor(centerFrame.midY - frameImage.size.height * 0.5)), size: frameImage.size) + strongSelf.batteryForegroundNode.frame = strongSelf.batteryBackgroundNode.frame + } + + if let sliderView = strongSelf.sliderView { + if themeUpdated { + sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor + sliderView.backColor = item.theme.list.itemSecondaryTextColor + sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45) + sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) + } + + sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) + } + + strongSelf.activateArea.accessibilityLabel = "Slider" + strongSelf.activateArea.accessibilityValue = centralMeasureText + strongSelf.activateArea.accessibilityTraits = .adjustable + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + @objc func sliderValueChanged() { + guard let sliderView = self.sliderView else { + return + } + self.item?.updated(Int32(rescaleSliderValueToPercentageValue(sliderView.value) * 100.0)) + } +} + diff --git a/Swiftgram/SGKeychainBackupManager/BUILD b/Swiftgram/SGKeychainBackupManager/BUILD new file mode 100644 index 00000000000..cd1a5d1293d --- /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 00000000000..2de2ebd5edf --- /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 00000000000..498396974c4 --- /dev/null +++ b/Swiftgram/SGLogging/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGLogging", + module_name = "SGLogging", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ManagedFile:ManagedFile" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGLogging/Sources/SGLogger.swift b/Swiftgram/SGLogging/Sources/SGLogger.swift new file mode 100644 index 00000000000..a3c8a09b174 --- /dev/null +++ b/Swiftgram/SGLogging/Sources/SGLogger.swift @@ -0,0 +1,183 @@ +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 class SGLogger { + public 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 + + public let rootPath: String + public 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 log(_ tag: String, _ what: @autoclosure () -> String) { + if !self.logToFile && !self.logToConsole { + return + } + + let string = what() + + var rawTime = time_t() + time(&rawTime) + var timeinfo = tm() + localtime_r(&rawTime, &timeinfo) + + var curTime = timeval() + gettimeofday(&curTime, nil) + let milliseconds = curTime.tv_usec / 1000 + + var consoleContent: String? + if self.logToConsole { + let content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string]) + consoleContent = content + print(content) + } + + if self.logToFile { + self.queue.async { + let content: String + if let consoleContent = consoleContent { + content = consoleContent + } else { + content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string]) + } + + var currentFile: ManagedFile? + var openNew = false + if let (file, length) = self.file { + if length >= self.maxLength { + self.file = nil + openNew = true + } else { + currentFile = file + } + } else { + openNew = true + } + if openNew { + let _ = try? FileManager.default.createDirectory(atPath: self.basePath, withIntermediateDirectories: true, attributes: nil) + + var createNew = false + if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) { + var minCreationDate: (Date, URL)? + var maxCreationDate: (Date, URL)? + var count = 0 + for url in files { + if url.lastPathComponent.hasPrefix("log-") { + if let values = try? url.resourceValues(forKeys: Set([URLResourceKey.creationDateKey])), let creationDate = values.creationDate { + count += 1 + if minCreationDate == nil || minCreationDate!.0 > creationDate { + minCreationDate = (creationDate, url) + } + if maxCreationDate == nil || maxCreationDate!.0 < creationDate { + maxCreationDate = (creationDate, url) + } + } + } + } + if let (_, url) = minCreationDate, count >= self.maxFiles { + let _ = try? FileManager.default.removeItem(at: url) + } + if let (_, url) = maxCreationDate { + var value = stat() + if stat(url.path, &value) == 0 && Int(value.st_size) < self.maxLength { + if let file = ManagedFile(queue: self.queue, path: url.path, mode: .append) { + self.file = (file, Int(value.st_size)) + currentFile = file + } + } else { + createNew = true + } + } else { + createNew = true + } + } + + if createNew { + let fileName = String(format: "log-%d-%d-%d_%02d-%02d-%02d.%03d.txt", arguments: [Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds)]) + + let path = self.basePath + "/" + fileName + + if let file = ManagedFile(queue: self.queue, path: path, mode: .append) { + self.file = (file, 0) + currentFile = file + } + } + } + + if let currentFile = currentFile { + if let data = content.data(using: .utf8) { + data.withUnsafeBytes { rawBytes -> Void in + let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + + let _ = currentFile.write(bytes, count: data.count) + } + var newline: UInt8 = 0x0a + let _ = currentFile.write(&newline, count: 1) + if let file = self.file { + self.file = (file.0, file.1 + data.count + 1) + } else { + assertionFailure() + } + } + } + } + } + } +} diff --git a/Swiftgram/SGLogging/Sources/Utils.swift b/Swiftgram/SGLogging/Sources/Utils.swift new file mode 100644 index 00000000000..68381110b15 --- /dev/null +++ b/Swiftgram/SGLogging/Sources/Utils.swift @@ -0,0 +1,6 @@ +//import Foundation +// +//public func extractNameFromPath(_ path: String) -> String { +// let fileName = URL(fileURLWithPath: path).lastPathComponent +// return String(fileName.prefix(upTo: fileName.lastIndex { $0 == "." } ?? fileName.endIndex)) +//} diff --git a/Swiftgram/SGLoggingComposer/BUILD b/Swiftgram/SGLoggingComposer/BUILD new file mode 100644 index 00000000000..bce950b1613 --- /dev/null +++ b/Swiftgram/SGLoggingComposer/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGLoggingComposer", + module_name = "SGLoggingComposer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGLogging:SGLogging", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGLoggingComposer/Sources/SGLoggingComposer.swift b/Swiftgram/SGLoggingComposer/Sources/SGLoggingComposer.swift new file mode 100644 index 00000000000..726f014db17 --- /dev/null +++ b/Swiftgram/SGLoggingComposer/Sources/SGLoggingComposer.swift @@ -0,0 +1,59 @@ +import Foundation +import SGLogging +import SwiftSignalKit + + +extension SGLogger { + 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 + } + } +} diff --git a/Swiftgram/SGPayWall/BUILD b/Swiftgram/SGPayWall/BUILD new file mode 100644 index 00000000000..d822f25b090 --- /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 00000000000..73c00596a7f --- /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 00000000000..06fbd1ff952 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 00000000000..558d4c78869 --- /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 00000000000..7c01e065c8e --- /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 00000000000..665a294eba5 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 00000000000..2c6fae70578 --- /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 00000000000..9945739af23 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 00000000000..7a1f30c724e --- /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 00000000000..300ec522b66 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 00000000000..3527b118da4 --- /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 00000000000..05e22393c79 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 00000000000..6c7c64d61cb --- /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 00000000000..56e048db075 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 00000000000..c76f57d4e72 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 00000000000..942d676df95 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 00000000000..eff6713cf99 --- /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 + + + +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" + + + +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))) + } +} + + + +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) + } +} + + + +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 +} + + + +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") + } + } +} + + + +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: .toolbar, title: "PayWall.InputToolbar.Title".i18n(lang), subtitle: "PayWall.InputToolbar.Notice".i18n(lang), description: "PayWall.InputToolbar.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: .icons, title: "PayWall.AppIcons.Title".i18n(lang), subtitle: "PayWall.AppIcons.Notice".i18n(lang), description: nil), + SGProFeature(id: .backup, title: "PayWall.SessionBackup.Title".i18n(lang), subtitle: "PayWall.SessionBackup.Notice".i18n(lang), description: "PayWall.SessionBackup.Description".i18n(lang)), + SGProFeature(id: .notifications, title: "PayWall.Notifications.Title".i18n(lang), subtitle: "PayWall.Notifications.Notice".i18n(lang), description: "PayWall.Notifications.Description".i18n(lang)), + ] + } + + 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) + } + } +} + + + +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)) + } +} + + + +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 + +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 + ) + } +} + + +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) + } + } +} + + +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 + } +} + + +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 + } +} + + +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 + } +} + + +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 + ) + } + } + } + ) + } +} + + +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 + } + } + } +} + + +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 + ) + ) + } +} + + +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 + +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 00000000000..a85f5f207b9 --- /dev/null +++ b/Swiftgram/SGProUI/BUILD @@ -0,0 +1,43 @@ +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/SettingsUI:SettingsUI", + # + "//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/AppBadgeSelectorController.swift b/Swiftgram/SGProUI/Sources/AppBadgeSelectorController.swift new file mode 100644 index 00000000000..dd985905859 --- /dev/null +++ b/Swiftgram/SGProUI/Sources/AppBadgeSelectorController.swift @@ -0,0 +1,149 @@ +import Foundation +import SwiftUI +import SGSwiftUI +import SGStrings +import SGSimpleSettings +import LegacyUI +import Display +import TelegramPresentationData +import AccountContext + + +struct AppBadge: Identifiable, Hashable { + let id: UUID = .init() + let displayName: String + let assetName: String +} + +func getAvailableAppBadges() -> [AppBadge] { + var appBadges: [AppBadge] = [ + .init(displayName: "Default", assetName: "Components/AppBadge"), + .init(displayName: "Sky", assetName: "SkyAppBadge"), + .init(displayName: "Night", assetName: "NightAppBadge"), + .init(displayName: "Titanium", assetName: "TitaniumAppBadge"), + .init(displayName: "Pro", assetName: "ProAppBadge"), + .init(displayName: "Day", assetName: "DayAppBadge"), + ] + + if SGSimpleSettings.shared.duckyAppIconAvailable { + appBadges.append(.init(displayName: "Ducky", assetName: "DuckyAppBadge")) + } + appBadges += [ + .init(displayName: "Sparkling", assetName: "SparklingAppBadge"), + ] + + return appBadges +} + +@available(iOS 14.0, *) +struct AppBadgeSettingsView: View { + weak var wrapperController: LegacyController? + let context: AccountContext + + @Environment(\.colorScheme) var colorScheme + @Environment(\.lang) var lang: String + + @State var selectedBadge: AppBadge + let availableAppBadges: [AppBadge] = getAvailableAppBadges() + + private enum Layout { + static let cardCorner: CGFloat = 12 + static let imageHeight: CGFloat = 56 + static let columnSpacing: CGFloat = 16 + static let horizontalPadding: CGFloat = 20 + } + + private var columns: [SwiftUI.GridItem] { + Array(repeating: GridItem(.flexible(), spacing: Layout.columnSpacing), count: 2) + } + + init(wrapperController: LegacyController?, context: AccountContext) { + self.wrapperController = wrapperController + self.context = context + + for badge in self.availableAppBadges { + if badge.assetName == SGSimpleSettings.shared.customAppBadge { + self._selectedBadge = State(initialValue: badge) + return + } + } + + self._selectedBadge = State(initialValue: self.availableAppBadges.first!) + } + + private func onSelectBadge(_ badge: AppBadge) { + self.selectedBadge = badge + let image = UIImage(bundleImageName: selectedBadge.assetName) ?? UIImage(bundleImageName: "Components/AppBadge") + if self.context.sharedContext.immediateSGStatus.status > 1 { + DispatchQueue.main.async { + SGSimpleSettings.shared.customAppBadge = selectedBadge.assetName + self.context.sharedContext.mainWindow?.badgeView.image = image + } + } + } + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, alignment: .center, spacing: Layout.columnSpacing) { + ForEach(availableAppBadges) { badge in + Button { + onSelectBadge(badge) + } label: { + VStack(spacing: 8) { + Image(badge.assetName) + .resizable() + .scaledToFit() + .frame(height: Layout.imageHeight) + .accessibilityHidden(true) + + Text(badge.displayName) + .font(.footnote) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color(colorScheme == .dark ? .secondarySystemBackground : .systemBackground)) + .cornerRadius(Layout.cardCorner) + .overlay( + RoundedRectangle(cornerRadius: Layout.cardCorner) + .stroke(selectedBadge == badge ? Color.accentColor : Color.clear, lineWidth: 2) + ) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, Layout.horizontalPadding) + .padding(.vertical, 24) + + } + .background(Color(colorScheme == .light ? .secondarySystemBackground : .systemBackground).ignoresSafeArea()) + } + +} + +@available(iOS 14.0, *) +public func sgAppBadgeSettingsController(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 = "AppBadge.Title".i18n(strings.baseLanguageCode) + + let swiftUIView = SGSwiftUIView( + legacyController: legacyController, + manageSafeArea: true, + content: { + AppBadgeSettingsView(wrapperController: legacyController, context: context) + } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) + + return legacyController +} diff --git a/Swiftgram/SGProUI/Sources/MessageFilterController.swift b/Swiftgram/SGProUI/Sources/MessageFilterController.swift new file mode 100644 index 00000000000..eb36ac2ef5a --- /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 00000000000..a8001251a28 --- /dev/null +++ b/Swiftgram/SGProUI/Sources/SGProUI.swift @@ -0,0 +1,194 @@ +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 +import SettingsUI + +// Optional +import SGSimpleSettings +import SGLogging + + +private enum SGProControllerSection: Int32, SGItemListSection { + case base + case appearance + case notifications + case footer +} + +private enum SGProDisclosureLink: String { + case sessionBackupManager + case messageFilter + case appIcons + case appBages +} + +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)) + entries.append(.header(id: id.count, section: .appearance, text: presentationData.strings.Appearance_Title.uppercased(), badge: nil)) + entries.append(.disclosure(id: id.count, section: .appearance, link: .appIcons, text: presentationData.strings.Appearance_AppIcon)) + entries.append(.disclosure(id: id.count, section: .appearance, link: .appBages, text: "AppBadge.Title".i18n(lang))) + entries.append(.notice(id: id.count, section: .appearance, text: "AppBadge.Notice".i18n(lang))) + + #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: + pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData)) + case .messageFilter: + pushControllerImpl?(sgMessageFilterController(presentationData: presentationData)) + case .appIcons: + pushControllerImpl?(themeSettingsController(context: context, focusOnItemTag: .icon)) + case .appBages: + if #available(iOS 14.0, *) { + pushControllerImpl?(sgAppBadgeSettingsController(context: context, 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 00000000000..dd42ff8ac85 --- /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 00000000000..ff5f233e309 --- /dev/null +++ b/Swiftgram/SGRegDate/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRegDate", + module_name = "SGRegDate", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGRegDateScheme:SGRegDateScheme", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//Swiftgram/SGDeviceToken:SGDeviceToken", + "//Swiftgram/SGStrings:SGStrings", + + "//submodules/AccountContext:AccountContext", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRegDate/Sources/SGRegDate.swift b/Swiftgram/SGRegDate/Sources/SGRegDate.swift new file mode 100644 index 00000000000..0be00196836 --- /dev/null +++ b/Swiftgram/SGRegDate/Sources/SGRegDate.swift @@ -0,0 +1,45 @@ +import Foundation +import SwiftSignalKit +import TelegramPresentationData + +import SGLogging +import SGStrings +import SGRegDateScheme +import AccountContext +import SGSimpleSettings +import SGAPI +import SGAPIToken +import SGDeviceToken + +public enum RegDateError { + case generic +} + +public func getRegDate(context: AccountContext, peerId: Int64) -> Signal { + return Signal { subscriber in + var tokensRequestSignal: Disposable? = nil + var apiRequestSignal: Disposable? = nil + if let regDateData = SGSimpleSettings.shared.regDateCache[String(peerId)], let regDate = try? JSONDecoder().decode(RegDate.self, from: regDateData), regDate.validUntil == 0 || regDate.validUntil > Int64(Date().timeIntervalSince1970) { + subscriber.putNext(regDate) + subscriber.putCompletion() + } else if SGSimpleSettings.shared.showRegDate { + tokensRequestSignal = combineLatest(getDeviceToken() |> mapError { error -> Void in SGLogger.shared.log("SGDeviceToken", "Error generating token: \(error)"); return Void() } , getSGApiToken(context: context) |> mapError { _ -> Void in return Void() }).start(next: { deviceToken, apiToken in + apiRequestSignal = getSGAPIRegDate(token: apiToken, deviceToken: deviceToken, userId: peerId).start(next: { regDate in + if let data = try? JSONEncoder().encode(regDate) { + SGSimpleSettings.shared.regDateCache[String(peerId)] = data + } + subscriber.putNext(regDate) + subscriber.putCompletion() + }) + }) + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + + return ActionDisposable { + tokensRequestSignal?.dispose() + apiRequestSignal?.dispose() + } + } +} diff --git a/Swiftgram/SGRegDateScheme/BUILD b/Swiftgram/SGRegDateScheme/BUILD new file mode 100644 index 00000000000..008f82658db --- /dev/null +++ b/Swiftgram/SGRegDateScheme/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRegDateScheme", + module_name = "SGRegDateScheme", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRegDateScheme/Sources/File.swift b/Swiftgram/SGRegDateScheme/Sources/File.swift new file mode 100644 index 00000000000..a972377e8bb --- /dev/null +++ b/Swiftgram/SGRegDateScheme/Sources/File.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct RegDate: Codable { + public let from: Int64 + public let to: Int64 + public let validUntil: Int64 +} diff --git a/Swiftgram/SGRequests/BUILD b/Swiftgram/SGRequests/BUILD new file mode 100644 index 00000000000..979d84f32e9 --- /dev/null +++ b/Swiftgram/SGRequests/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGRequests", + module_name = "SGRequests", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit" + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGRequests/Sources/File.swift b/Swiftgram/SGRequests/Sources/File.swift new file mode 100644 index 00000000000..19dfa3da279 --- /dev/null +++ b/Swiftgram/SGRequests/Sources/File.swift @@ -0,0 +1,72 @@ +import Foundation +import SwiftSignalKit + + +public func requestsDownload(url: URL) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + + let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, response, error in + let _ = completed.swap(true) + if let location = location, let data = try? Data(contentsOf: location) { + subscriber.putNext((data, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + downloadTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + downloadTask.cancel() + } + } + } +} + +public func requestsGet(url: URL) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + + let urlTask = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in + let _ = completed.swap(true) + if let strongData = data { + subscriber.putNext((strongData, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + urlTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + urlTask.cancel() + } + } + } +} + + +public func requestsCustom(request: URLRequest) -> Signal<(Data, URLResponse?), Error?> { + return Signal { subscriber in + let completed = Atomic(value: false) + let urlTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in + _ = completed.swap(true) + if let strongData = data { + subscriber.putNext((strongData, response)) + subscriber.putCompletion() + } else { + subscriber.putError(error) + } + }) + urlTask.resume() + + return ActionDisposable { + if !completed.with({ $0 }) { + urlTask.cancel() + } + } + } +} diff --git a/Swiftgram/SGSettingsBundle/BUILD b/Swiftgram/SGSettingsBundle/BUILD new file mode 100644 index 00000000000..e0d37a3c515 --- /dev/null +++ b/Swiftgram/SGSettingsBundle/BUILD @@ -0,0 +1,10 @@ +load("@build_bazel_rules_apple//apple:resources.bzl", "apple_bundle_import") + +apple_bundle_import( + name = "SGSettingsBundle", + bundle_imports = glob([ + "Settings.bundle/*", + "Settings.bundle/**/*", + ]), + visibility = ["//visibility:public"] +) \ No newline at end of file diff --git a/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist b/Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist new file mode 100644 index 00000000000..148a22836fd --- /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 00000000000..e40aa8c2500 --- /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 00000000000..a0f39d27b40 --- /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 00000000000..dc1613e7810 --- /dev/null +++ b/Swiftgram/SGSettingsUI/BUILD @@ -0,0 +1,43 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +filegroup( + name = "SGUIAssets", + srcs = glob(["Images.xcassets/**"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "SGSettingsUI", + module_name = "SGSettingsUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//Swiftgram/SGItemListUI:SGItemListUI", + "//Swiftgram/SGLogging:SGLogging", + "//Swiftgram/SGSimpleSettings:SGSimpleSettings", + "//Swiftgram/SGStrings:SGStrings", +# "//Swiftgram/SGAPI:SGAPI", + "//Swiftgram/SGAPIToken:SGAPIToken", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/MtProtoKit:MtProtoKit", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ItemListUI:ItemListUI", + "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/OverlayStatusController:OverlayStatusController", + "//submodules/AccountContext:AccountContext", + "//submodules/AppBundle:AppBundle", + "//submodules/TelegramUI/Components/Settings/PeerNameColorScreen", + "//submodules/UndoUI:UndoUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json new file mode 100644 index 00000000000..526cf46d7c8 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lt_savetocloud.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf new file mode 100644 index 00000000000..ed4efd9629f Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf differ diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json new file mode 100644 index 00000000000..6fb419fc51b --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "swiftgram_context_menu.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf new file mode 100644 index 00000000000..30789ecb778 Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf differ diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json new file mode 100644 index 00000000000..7506e639ebf --- /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 00000000000..fb4264fd56b 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 00000000000..1bf20b6bc87 --- /dev/null +++ b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "Swiftgram.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } + } + \ No newline at end of file diff --git a/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf new file mode 100644 index 00000000000..6abd681bf69 Binary files /dev/null and b/Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf differ diff --git a/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift new file mode 100644 index 00000000000..8431704df1a --- /dev/null +++ b/Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift @@ -0,0 +1,735 @@ +// 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 voiceMessages + 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 + case translationBackend + case transcriptionBackend +} + +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.showRepostToStoryV2, 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(.oneFromManySelector(id: id.count, section: .translation, settingName: .translationBackend, text: i18n("Settings.Translation.Backend", lang), value: i18n("Settings.Translation.Backend.\(SGSimpleSettings.shared.translationBackend)", lang), enabled: true)) + if SGSimpleSettings.shared.translationBackendEnum != .gtranslate { + entries.append(.notice(id: id.count, section: .translation, text: i18n("Settings.Translation.Backend.Notice", lang, "Settings.Translation.Backend.\(SGSimpleSettings.TranslationBackend.gtranslate.rawValue)".i18n(lang)))) + } else { + id.increment(1) + } + 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: .voiceMessages, text: "Settings.Transcription.Header".i18n(lang), badge: nil)) + entries.append(.oneFromManySelector(id: id.count, section: .voiceMessages, settingName: .transcriptionBackend, text: i18n("Settings.Transcription.Backend", lang), value: i18n("Settings.Transcription.Backend.\(SGSimpleSettings.shared.transcriptionBackend)", lang), enabled: true)) + if SGSimpleSettings.shared.transcriptionBackendEnum != .apple { + entries.append(.notice(id: id.count, section: .voiceMessages, text: i18n("Settings.Transcription.Backend.Notice", lang, "Settings.Transcription.Backend.\(SGSimpleSettings.TranscriptionBackend.apple.rawValue)".i18n(lang)))) + } else { + id.increment(1) + } + entries.append(.header(id: id.count, section: .voiceMessages, text: presentationData.strings.Privacy_VoiceMessages.uppercased(), badge: nil)) + entries.append(.toggle(id: id.count, section: .voiceMessages, settingName: .forceBuiltInMic, value: SGSimpleSettings.shared.forceBuiltInMic, text: i18n("Settings.forceBuiltInMic", lang), enabled: true)) + entries.append(.notice(id: id.count, section: .voiceMessages, text: i18n("Settings.forceBuiltInMic.Notice", lang))) + + 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: .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.showRepostToStoryV2 = 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) +// })) +// } + case .translationBackend: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.translationBackend = value + simplePromise.set(true) + } + + for value in SGSimpleSettings.TranslationBackend.allCases { + if value == .system { + if #available(iOS 18.0, *) { + } else { + continue // System translation is not available on iOS 17 and below + } + } + items.append(ActionSheetButtonItem(title: i18n("Settings.Translation.Backend.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } + case .transcriptionBackend: + let setAction: (String) -> Void = { value in + SGSimpleSettings.shared.transcriptionBackend = value + simplePromise.set(true) + } + + for value in SGSimpleSettings.TranscriptionBackend.allCases { + if #available(iOS 13.0, *) { + } else { + if value == .apple { + continue // Apple recognition is not available on iOS 12 + } + } + items.append(ActionSheetButtonItem(title: i18n("Settings.Transcription.Backend.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + setAction(value.rawValue) + })) + } + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, openDisclosureLink: { link in + switch (link) { + case .languageSettings: + pushControllerImpl?(context.sharedContext.makeLocalizationListController(context: context)) + case .contentSettings: + let _ = (getSGSettingsURL(context: context) |> deliverOnMainQueue).start(next: { [weak context] url in + guard let strongContext = context else { + return + } + strongContext.sharedContext.applicationBindings.openUrl(url) + }) + } + }, searchInput: { searchQuery in + updateState { state in + var updatedState = state + updatedState.searchQuery = searchQuery + return updatedState + } + }) + + let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) + let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings, PreferencesKeys.appConfiguration]) + let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network) + |> map(Optional.init) + let contentSettingsConfiguration = Promise() + contentSettingsConfiguration.set(.single(nil) + |> then(updatedContentSettingsConfiguration)) + + let signal = combineLatest(simplePromise.get(), /*sliderPromise.get(),*/ statePromise.get(), context.sharedContext.presentationData, sharedData, preferences, contentSettingsConfiguration.get(), + context.engine.accountData.observeAvailableColorOptions(scope: .replies), + context.engine.accountData.observeAvailableColorOptions(scope: .profile) + ) + |> map { _, /*sliderValue,*/ state, presentationData, sharedData, view, contentSettingsConfiguration, availableReplyColors, availableProfileColors -> (ItemListControllerState, (ItemListNodeState, Any)) in + + let sgUISettings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? SGUISettings.default + let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let callListSettings: CallListSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) ?? CallListSettings.defaultSettings + let experimentalUISettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + + let entries = SGControllerEntries(presentationData: presentationData, callListSettings: callListSettings, experimentalUISettings: experimentalUISettings, SGSettings: sgUISettings, appConfiguration: appConfiguration, nameColors: PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors), state: state) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + + // TODO(swiftgram): focusOnItemTag support + /* var index = 0 + var scrollToItem: ListViewScrollToItem? + if let focusOnItemTag = focusOnItemTag { + for entry in entries { + if entry.tag?.isEqual(to: focusOnItemTag) ?? false { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) + } + index += 1 + } + } */ + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ ) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } +// getRootControllerImpl = { [weak controller] in +// return controller?.view.window?.rootViewController +// } +// getNavigationControllerImpl = { [weak controller] in +// return controller?.navigationController as? NavigationController +// } + askForRestart = { [weak context] in + guard let context = context else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?( + UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, // i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode), + text: i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode), + timeout: nil, + customUndoText: i18n("Common.RestartNow", presentationData.strings.baseLanguageCode) //presentationData.strings.Common_Yes + ), + elevatedLayout: false, + action: { action in if action == .undo { exit(0) }; return true } + ), + nil + ) + } + return controller + +} diff --git a/Swiftgram/SGShowMessageJson/BUILD b/Swiftgram/SGShowMessageJson/BUILD new file mode 100644 index 00000000000..8097e4c906a --- /dev/null +++ b/Swiftgram/SGShowMessageJson/BUILD @@ -0,0 +1,9 @@ +filegroup( + name = "SGShowMessageJson", + srcs = glob([ + "Sources/**/*.swift", + ]), + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift b/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift new file mode 100644 index 00000000000..7868b0db3ad --- /dev/null +++ b/Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift @@ -0,0 +1,76 @@ +import Foundation +import Wrap +import SGLogging +import ChatControllerInteraction +import ChatPresentationInterfaceState +import Postbox +import TelegramCore +import AccountContext + +public func showMessageJson(controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, message: Message, context: AccountContext) { + if let navigationController = controllerInteraction.navigationController(), let rootController = navigationController.view.window?.rootViewController { + var writingOptions: JSONSerialization.WritingOptions = [ + .prettyPrinted, + //.sortedKeys, + ] + if #available(iOS 13.0, *) { + writingOptions.insert(.withoutEscapingSlashes) + } + + var messageData: Data? = nil + do { + messageData = try wrap( + message, + writingOptions: writingOptions + ) + } catch { + SGLogger.shared.log("ShowMessageJSON", "Error parsing data: \(error)") + messageData = nil + } + + guard let messageData = messageData else { return } + + let id = Int64.random(in: Int64.min ... Int64.max) + let fileResource = LocalFileMediaResource(fileId: id, size: Int64(messageData.count), isSecretRelated: false) + context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: messageData, synchronous: true) + + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/json; charset=utf-8", size: Int64(messageData.count), attributes: [.FileName(fileName: "message.json")], alternativeRepresentations: []) + + presentDocumentPreviewController(rootController: rootController, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, postbox: context.account.postbox, file: file, canShare: !message.isCopyProtected()) + + } +} + +extension MemoryBuffer: @retroactive WrapCustomizable { + + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + let hexString = self.description + return ["string": hexStringToString(hexString) ?? hexString] + } +} + +// There's a chacne we will need it for each empty/weird type, or it will be a runtime crash. +extension ContentRequiresValidationMessageAttribute: @retroactive WrapCustomizable { + + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return ["@type": "ContentRequiresValidationMessageAttribute"] + } +} + +func hexStringToString(_ hexString: String) -> String? { + var chars = Array(hexString) + var result = "" + + while chars.count > 0 { + let c = String(chars[0...1]) + chars = Array(chars.dropFirst(2)) + if let byte = UInt8(c, radix: 16) { + let scalar = UnicodeScalar(byte) + result.append(String(scalar)) + } else { + return nil + } + } + + return result +} diff --git a/Swiftgram/SGSimpleSettings/BUILD b/Swiftgram/SGSimpleSettings/BUILD new file mode 100644 index 00000000000..cd8266c43a6 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/BUILD @@ -0,0 +1,19 @@ +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", + "//Swiftgram/SGLogging:SGLogging", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift b/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift new file mode 100644 index 00000000000..b0d073605dc --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift @@ -0,0 +1,58 @@ +//// A copy of Atomic from SwiftSignalKit +//import Foundation +// +//public enum AtomicWrapperLockError: Error { +// case isLocked +//} +// +//public final class AtomicWrapper { +// private var lock: pthread_mutex_t +// private var value: T +// +// public init(value: T) { +// self.lock = pthread_mutex_t() +// self.value = value +// +// pthread_mutex_init(&self.lock, nil) +// } +// +// deinit { +// pthread_mutex_destroy(&self.lock) +// } +// +// public func with(_ f: (T) -> R) -> R { +// pthread_mutex_lock(&self.lock) +// let result = f(self.value) +// pthread_mutex_unlock(&self.lock) +// +// return result +// } +// +// public func tryWith(_ f: (T) -> R) throws -> R { +// if pthread_mutex_trylock(&self.lock) == 0 { +// let result = f(self.value) +// pthread_mutex_unlock(&self.lock) +// return result +// } else { +// throw AtomicWrapperLockError.isLocked +// } +// } +// +// public func modify(_ f: (T) -> T) -> T { +// pthread_mutex_lock(&self.lock) +// let result = f(self.value) +// self.value = result +// pthread_mutex_unlock(&self.lock) +// +// return result +// } +// +// public func swap(_ value: T) -> T { +// pthread_mutex_lock(&self.lock) +// let previous = self.value +// self.value = value +// pthread_mutex_unlock(&self.lock) +// +// return previous +// } +//} diff --git a/Swiftgram/SGSimpleSettings/Sources/RWLock.swift b/Swiftgram/SGSimpleSettings/Sources/RWLock.swift new file mode 100644 index 00000000000..3ea2436c6f5 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/RWLock.swift @@ -0,0 +1,36 @@ +// +// RWLock.swift +// SwiftConcurrentCollections +// +// Created by Pete Prokop on 09/02/2020. +// Copyright © 2020 Pete Prokop. All rights reserved. +// + +import Foundation + +public final class RWLock { + private var lock: pthread_rwlock_t + + // MARK: Lifecycle + deinit { + pthread_rwlock_destroy(&lock) + } + + public init() { + lock = pthread_rwlock_t() + pthread_rwlock_init(&lock, nil) + } + + // MARK: Public + public func writeLock() { + pthread_rwlock_wrlock(&lock) + } + + public func readLock() { + pthread_rwlock_rdlock(&lock) + } + + public func unlock() { + pthread_rwlock_unlock(&lock) + } +} diff --git a/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift new file mode 100644 index 00000000000..499ad957f6b --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift @@ -0,0 +1,579 @@ +import Foundation +import SGAppGroupIdentifier +import SGLogging + +let APP_GROUP_IDENTIFIER = sgAppGroupIdentifier() + +public class SGSimpleSettings { + + public static let shared = SGSimpleSettings() + + private init() { + setDefaultValues() + migrate() + 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 migrate() { + let showRepostToStoryMigrationKey = "migrated_\(Keys.showRepostToStory.rawValue)" + if let groupUserDefaults = UserDefaults(suiteName: APP_GROUP_IDENTIFIER) { + if !groupUserDefaults.bool(forKey: showRepostToStoryMigrationKey) { + self.showRepostToStoryV2 = self.showRepostToStory + groupUserDefaults.set(true, forKey: showRepostToStoryMigrationKey) + SGLogger.shared.log("SGSimpleSettings", "Migrated showRepostToStory. \(self.showRepostToStory) -> \(self.showRepostToStoryV2)") + } + } else { + SGLogger.shared.log("SGSimpleSettings", "Unable to migrate showRepostToStory. Shared UserDefaults suite is not available for '\(APP_GROUP_IDENTIFIER)'.") + } + } + + 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 }, + { let _ = self.customAppBadge } + ] + + 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 showRepostToStoryV2 + 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 + case transcriptionBackend + case translationBackend + case customAppBadge + } + + 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 TranscriptionBackend: String, CaseIterable { + case `default` + case apple + } + + public enum TranslationBackend: String, CaseIterable { + case `default` + case gtranslate + case system + // Make sure to update TranslationConfiguration + } + + 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, + Keys.transcriptionBackend.rawValue: TranscriptionBackend.default.rawValue, + Keys.translationBackend.rawValue: TranslationBackend.default.rawValue, + Keys.customAppBadge.rawValue: "", + ] + + 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, + Keys.showRepostToStoryV2.rawValue: true, + ] + + @UserDefault(key: Keys.hidePhoneInSettings.rawValue) + public var hidePhoneInSettings: Bool + + @UserDefault(key: Keys.showTabNames.rawValue) + public var showTabNames: Bool + + @UserDefault(key: Keys.startTelescopeWithRearCam.rawValue) + public var startTelescopeWithRearCam: Bool + + @UserDefault(key: Keys.accountColorsSaturation.rawValue) + public var accountColorsSaturation: Int32 + + @UserDefault(key: Keys.uploadSpeedBoost.rawValue) + public var uploadSpeedBoost: Bool + + @UserDefault(key: Keys.downloadSpeedBoost.rawValue) + public var downloadSpeedBoost: String + + @UserDefault(key: Keys.rememberLastFolder.rawValue) + public var rememberLastFolder: Bool + + @UserDefault(key: Keys.bottomTabStyle.rawValue) + public var bottomTabStyle: String + + public var lastAccountFolders = UserDefaultsBackedDictionary(userDefaultsKey: Keys.lastAccountFolders.rawValue, threadSafe: false) + + @UserDefault(key: Keys.localDNSForProxyHost.rawValue) + public var localDNSForProxyHost: Bool + + @UserDefault(key: Keys.sendLargePhotos.rawValue) + public var sendLargePhotos: Bool + + @UserDefault(key: Keys.outgoingPhotoQuality.rawValue) + public var outgoingPhotoQuality: Int32 + + @UserDefault(key: Keys.storyStealthMode.rawValue) + public var storyStealthMode: Bool + + @UserDefault(key: Keys.canUseStealthMode.rawValue) + public var canUseStealthMode: Bool + + @UserDefault(key: Keys.disableSwipeToRecordStory.rawValue) + public var disableSwipeToRecordStory: Bool + + @UserDefault(key: Keys.quickTranslateButton.rawValue) + public var quickTranslateButton: Bool + + public var outgoingLanguageTranslation = UserDefaultsBackedDictionary(userDefaultsKey: Keys.outgoingLanguageTranslation.rawValue, threadSafe: false) + + @UserDefault(key: Keys.hideReactions.rawValue) + public var hideReactions: Bool + + // @available(*, deprecated, message: "Use showRepostToStoryV2 instead") + @UserDefault(key: Keys.showRepostToStory.rawValue) + public var showRepostToStory: Bool + + @UserDefault(key: Keys.showRepostToStoryV2.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard) + public var showRepostToStoryV2: 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 + + @UserDefault(key: Keys.transcriptionBackend.rawValue) + public var transcriptionBackend: String + + @UserDefault(key: Keys.translationBackend.rawValue) + public var translationBackend: String + + @UserDefault(key: Keys.customAppBadge.rawValue) + public var customAppBadge: String +} + +extension SGSimpleSettings { + public var isStealthModeEnabled: Bool { + return storyStealthMode && canUseStealthMode + } + + public static func makeOutgoingLanguageTranslationKey(accountId: Int64, peerId: Int64) -> String { + return "\(accountId):\(peerId)" + } +} + +extension SGSimpleSettings { + public var translationBackendEnum: SGSimpleSettings.TranslationBackend { + return TranslationBackend(rawValue: translationBackend) ?? .default + } + + public var transcriptionBackendEnum: SGSimpleSettings.TranscriptionBackend { + return TranscriptionBackend(rawValue: transcriptionBackend) ?? .default + } +} + +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 00000000000..63784ff3e66 --- /dev/null +++ b/Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift @@ -0,0 +1,408 @@ +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 Int64.Type: + return (Int64(exactly: userDefaults.integer(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 00000000000..acef413a33a --- /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 00000000000..6bedd862a50 --- /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 00000000000..dea968818af --- /dev/null +++ b/Swiftgram/SGStrings/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGStrings", + module_name = "SGStrings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AppBundle:AppBundle", + "//Swiftgram/SGLogging:SGLogging" + ], + visibility = [ + "//visibility:public", + ], +) + +filegroup( + name = "SGLocalizableStrings", + srcs = glob(["Strings/*.lproj/SGLocalizable.strings"]), + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGStrings/Sources/LocalizationManager.swift b/Swiftgram/SGStrings/Sources/LocalizationManager.swift new file mode 100644 index 00000000000..331586aa410 --- /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 00000000000..5acfe970d5d --- /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 00000000000..41f11684547 --- /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 00000000000..a48c45fa921 --- /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 00000000000..05cf6ed4827 --- /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 00000000000..cb0c4174db9 --- /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 00000000000..726e485dcfb --- /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 00000000000..010c2fd7edb --- /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 00000000000..613913b037e --- /dev/null +++ b/Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings @@ -0,0 +1,263 @@ +"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"; +"Settings.Translation.Backend" = "Service"; +/* Do not translate */ +"Settings.Translation.Backend.default" = "Telegram"; +/* Do not translate */ +"Settings.Translation.Backend.gtranslate" = "GTranslate"; +"Settings.Translation.Backend.system" = "System"; +"Settings.Translation.Backend.Notice" = "Swiftgram will fallback to %@ if selected translation service is not available."; + +"Settings.Transcription.Header" = "VOICE-TO-TEXT"; +"Settings.Transcription.Backend" = "Service"; +/* Do not translate */ +"Settings.Transcription.Backend.default" = "Telegram"; +/* Do not translate */ +"Settings.Transcription.Backend.apple" = "Apple"; +"Settings.Transcription.Backend.Notice" = "Swiftgram will fallback to %@ if selected transcription service is not available."; + +"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 HD 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 and Badges"; +"PayWall.AppIcons.Notice" = "Customize Swiftgram look on your home screen and screenshots."; + +"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."; + +"AppBadge.Title" = "App Badge"; +"AppBadge.Notice" = "Customize App Badge shown on screenshots"; diff --git a/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..4fcb6aee082 --- /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 00000000000..1581d635363 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fa.lproj/SGLocalizable.strings @@ -0,0 +1,9 @@ +"Settings.Tabs.Header" = "زبانه ها"; +"Settings.Tabs.ShowContacts" = "نمایش برگه مخاطبین"; +"Settings.VideoNotes.Header" = "فیلم های round"; +"Settings.Tabs.ShowNames" = "نشان دادن برگه اسم ها"; +"Settings.HidePhoneInSettingsUI" = "پنهان کردن شماره موبایل در تنظیمات"; +"Settings.HidePhoneInSettingsUI.Notice" = "شماره شما فقط در رابط کاربری پنهان خواهد شد. برای پنهان کردن آن از دید دیگران ، لطفاً از تنظیمات حریم خصوصی استفاده کنید."; +"Settings.ShowProfileID" = "نمایش ایدی پروفایل"; +"Settings.Translation.QuickTranslateButton" = "دکمه ترجمه سریع"; +"ContextMenu.SaveToCloud" = "ذخیره در فضای ابری"; diff --git a/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..3e7ea96fbf1 --- /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 00000000000..adc9a1b3d3b --- /dev/null +++ b/Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings @@ -0,0 +1,137 @@ +"Settings.ContentSettings" = "Paramètres du contenu"; + +"Settings.Tabs.Header" = "ONGLETS"; +"Settings.Tabs.HideTabBar" = "Masquer la barre d'onglets"; +"Settings.Tabs.ShowContacts" = "Afficher l'onglet Contacts"; +"Settings.Tabs.ShowNames" = "Afficher les noms des onglets"; + +"Settings.Folders.BottomTab" = "Dossiers en bas"; +"Settings.Folders.BottomTabStyle" = "Style des dossiers inférieurs"; + +/* Do not translate */ +"Settings.Folders.BottomTabStyle.ios" = "iOS"; +/* Do not translate */ +"Settings.Folders.BottomTabStyle.telegram" = "Telegram"; +/* Example: Hide "All Chats" */ +"Settings.Folders.AllChatsHidden" = "Masquer \"%@\""; +"Settings.Folders.RememberLast" = "Ouvrir le dernier dossier"; +"Settings.Folders.RememberLast.Notice" = "Swiftgram ouvrira le dernier dossier utilisé après le redémarrage ou changement de compte"; + +"Settings.Folders.CompactNames" = "Espacement plus petit"; +"Settings.Folders.AllChatsTitle" = "Titre \"Tous les Chats\""; +"Settings.Folders.AllChatsTitle.short" = "Courte"; +"Settings.Folders.AllChatsTitle.long" = "Longue"; +/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */ +"Settings.Folders.AllChatsTitle.none" = "Par défaut"; + + +"Settings.ChatList.Header" = "LISTE DE CHAT"; +"Settings.CompactChatList" = "Liste de discussion compacte"; + +"Settings.Profiles.Header" = "PROFILES"; + +"Settings.Stories.Hide" = "Cacher les histoires"; +"Settings.Stories.WarnBeforeView" = "Demander avant de visionner"; +"Settings.Stories.DisableSwipeToRecord" = "Désactiver le glissement pour enregistrer"; + +"Settings.Translation.QuickTranslateButton" = "Bouton de traduction rapide"; + +"Stories.Warning.Author" = "Auteur"; +"Stories.Warning.ViewStory" = "Voir l'histoire?"; +/* Author will be able to see that you viewed their Story */ +"Stories.Warning.Notice" = "%@ SERA autorisé à voir que vous avez vu son histoire."; +"Stories.Warning.NoticeStealth" = "%@ ne sera pas en mesure de voir que vous avez vu leur Histoire."; + +"Settings.Photo.Quality.Notice" = "Qualité des photos et des récits photo sortants"; +"Settings.Photo.SendLarge" = "Envoyer de grandes photos"; +"Settings.Photo.SendLarge.Notice" = "Augmenter la limite latérale des images compressées à 2560px"; + +"Settings.VideoNotes.Header" = "VIDÉOS RONDES"; +"Settings.VideoNotes.StartWithRearCam" = "Commencer avec la caméra arrière"; + +"Settings.CustomColors.Header" = "COULEURS DU COMPTE"; +"Settings.CustomColors.Saturation" = "SATURATION"; +/* Make sure to escape Percentage sign % */ +"Settings.CustomColors.Saturation.Notice" = "Régler la saturation à 0%% pour désactiver les couleurs du compte"; + +"Settings.UploadsBoost" = "Chargements boost"; +"Settings.DownloadsBoost" = "Boost de téléchargements"; +"Settings.DownloadsBoost.none" = "Désactivé"; +"Settings.DownloadsBoost.medium" = "Moyenne"; +"Settings.DownloadsBoost.maximum" = "Maximum"; + +"Settings.ShowProfileID" = "Afficher l'identifiant du profil"; +"Settings.ShowDC" = "Afficher le centre de données"; +"Settings.ShowCreationDate" = "Afficher la date de création du chat"; +"Settings.ShowCreationDate.Notice" = "La date de création peut être inconnue pour certains chats."; + +"Settings.ShowRegDate" = "Afficher la date d'inscription"; +"Settings.ShowRegDate.Notice" = "La date d'inscription est approximative."; + +"Settings.SendWithReturnKey" = "Envoyer avec la clé \"return\""; +"Settings.HidePhoneInSettingsUI" = "Masquer le téléphone dans les paramètres"; +"Settings.HidePhoneInSettingsUI.Notice" = "Votre numéro sera masqué dans l'interface utilisateur uniquement. Pour le masquer aux autres, veuillez utiliser les paramètres de confidentialité."; + +"PasscodeSettings.AutoLock.InFiveSeconds" = "Si absente pendant 5 secondes"; + +"ProxySettings.UseSystemDNS" = "Utiliser le DNS du système"; +"ProxySettings.UseSystemDNS.Notice" = "Utiliser le DNS système pour contourner le délai d'attente si vous n'avez pas accès à Google DNS"; + +/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */ +"Common.NoTelegramPremiumNeeded" = "Vous **n'avez pas besoin** %@!"; +"Common.RestartRequired" = "Redémarrage nécessaire"; +"Common.RestartNow" = "Redémarrer maintenant"; +"Common.OpenTelegram" = "Ouvrir Telegram"; +"Common.UseTelegramForPremium" = "Veuillez noter que pour obtenir Telegram Premium, vous devez utiliser l'application Telegram officielle. Une fois que vous avez obtenu Telegram Premium, toutes ses fonctionnalités seront disponibles dans Swiftgram."; + +"Message.HoldToShowOrReport" = "Maintenir pour afficher ou rapporter."; + +"Auth.AccountBackupReminder" = "Assurez-vous d'avoir une méthode d'accès de sauvegarde. Gardez une carte SIM pour les SMS ou une session supplémentaire connectée pour éviter d'être bloquée."; +"Auth.UnofficialAppCodeTitle" = "Vous ne pouvez obtenir le code qu'avec l'application officielle"; + +"Settings.SmallReactions" = "Petites réactions"; +"Settings.HideReactions" = "Masquer les réactions"; + +"ContextMenu.SaveToCloud" = "Sauvegarder dans le cloud"; +"ContextMenu.SelectFromUser" = "Sélectionner de l'Auteur"; + +"Settings.ContextMenu" = "MENU CONTEXTUEL"; +"Settings.ContextMenu.Notice" = "Les entrées désactivées seront disponibles dans le sous-menu 'Swiftgram'."; + + +"Settings.ChatSwipeOptions" = "Options de balayage de la liste de chat"; +"Settings.DeleteChatSwipeOption" = "Glisser pour supprimer la conversation"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */ +"Settings.PullToNextChannel" = "Tirer vers le prochain canal non lu"; +/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */ +"Settings.PullToNextTopic" = "Tirer pour le sujet suivant"; +"Settings.GalleryCamera" = "Appareil photo dans la galerie"; +/* "Send Message As..." button */ +"Settings.SendAsButton" = "Bouton \"%@\""; +"Settings.SnapDeletionEffect" = "Effets de suppression de message"; + +"Settings.Stickers.Size" = "TAILLE"; +"Settings.Stickers.Timestamp" = "Afficher l'horodatage"; + +"Settings.RecordingButton" = "Bouton d'enregistrement vocal"; + +"Settings.DefaultEmojisFirst" = "Prioriser les emojis standard"; +"Settings.DefaultEmojisFirst.Notice" = "Afficher les emojis standard avant les emojis premium dans le clavier emoji"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "créé: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Rejoint %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Enregistré"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Appuyez deux fois pour modifier le message"; + +"Settings.wideChannelPosts" = "Messages larges dans les canaux"; +"Settings.ForceEmojiTab" = "Clavier emoji par défaut"; + +"Settings.forceBuiltInMic" = "Forcer le microphone de l'appareil"; +"Settings.forceBuiltInMic.Notice" = "Si activé, l'application utilisera uniquement le microphone de l'appareil même si des écouteurs sont connectés."; + +"Settings.hideChannelBottomButton" = "Masquer le panneau inférieur du canal"; diff --git a/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..eb4562b7c89 --- /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 00000000000..6adc148a1d1 --- /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 00000000000..d357bb69b61 --- /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 00000000000..44ba7a11a2f --- /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 00000000000..ca32eafb8cf --- /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 00000000000..afe45d65669 --- /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 00000000000..928cf393a67 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/km.lproj/SGLocalizable.strings @@ -0,0 +1,8 @@ +"Settings.Tabs.Header" = "ថេប"; +"Settings.Tabs.ShowContacts" = "បង្ហាញថេបទំនាក់ទំនង"; +"Settings.VideoNotes.Header" = "រង្វង់វីដេអូ"; +"Settings.VideoNotes.StartWithRearCam" = "ចាប់ផ្ដើមជាមួយកាមេរ៉ាក្រោយ"; +"Settings.Tabs.ShowNames" = "បង្ហាញឈ្មោះថេប"; +"Settings.HidePhoneInSettingsUI" = "លាក់លេខទូរសព្ទក្នុងការកំណត់"; +"Settings.Folders.BottomTab" = "ថតឯបាត"; +"ContextMenu.SaveToCloud" = "រក្សាទុកទៅពពក"; diff --git a/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ko.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..501a5f64b49 --- /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 00000000000..62ac20a89c4 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/ku.lproj/SGLocalizable.strings @@ -0,0 +1,10 @@ +"Settings.Tabs.Header" = "تابەکان"; +"Settings.Tabs.ShowContacts" = "نیشاندانی تابی کۆنتاکتەکان"; +"Settings.VideoNotes.Header" = "ڤیدیۆ بازنەییەکان"; +"Settings.VideoNotes.StartWithRearCam" = "دەستپێکردن بە کامێرای پشتەوە"; +"Settings.Tabs.ShowNames" = "نیشاندانی ناوی تابەکان"; +"Settings.HidePhoneInSettingsUI" = "شاردنەوەی تەلەفۆن لە ڕێکخستنەکان"; +"Settings.HidePhoneInSettingsUI.Notice" = "ژمارەکەت تەنها لە ڕووکارەکە دەرناکەوێت. بۆ ئەوەی لە ئەوانەی دیکەی بشاریتەوە، تکایە ڕێکخستنەکانی پارێزراوی بەکاربێنە."; +"Settings.Translation.QuickTranslateButton" = "دوگمەی وەرگێڕانی خێرا"; +"Settings.Folders.BottomTab" = "بوخچەکان لە خوارەوە"; +"ContextMenu.SaveToCloud" = "هەڵگرتن لە کڵاود"; diff --git a/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/nl.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..d80e6ca49e2 --- /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 00000000000..5fd16d5c627 --- /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 00000000000..9efb5c22477 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pl.lproj/SGLocalizable.strings @@ -0,0 +1,245 @@ +"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ługi"; +/* 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" = "PROFILE"; + +"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" = "Pokazać 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" = "Dla niektórych czatów data utworzenia może być nieznana."; + +"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" = "Aby otrzymać Telegram Premium, musisz skorzystać z oficjalnej aplikacji Telegram. Po uzyskaniu Telegram Premium wszystkie jego funkcje staną się dostępne w Swiftgram."; +"Common.UpdateOS" = "Wymagana aktualizacja iOS"; + +"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"; +"Settings.GalleryCameraPreview" = "Podgląd aparatu 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 emoji"; +"Settings.DefaultEmojisFirst.Notice" = "Na klawiaturze emoji pokaż standardowe emoji przed Premium"; + +/* Date when chat was created. "created: 24 May 2016" */ +"Chat.Created" = "utworzony: %@"; + +/* Date when user joined the chat. "Joined Swiftgram Chat" */ +"Chat.JoinedDateTitle" = "Dołączono %@"; +/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */ +"Chat.RegDate" = "Zarejestrowano"; + +"Settings.messageDoubleTapActionOutgoingEdit" = "Podwójne stuknięcie, aby edytować wiadomość"; + +"Settings.wideChannelPosts" = "Szerokie wpisy 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.showChannelBottomButton" = "Dolny panel kanału"; + +"Settings.secondsInMessages" = "Sekundy w wiadomościach"; + +"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" = "wzajemny kontakt"; + +"Settings.swipeForVideoPIP" = "Wideo PIP po przesunięciu"; +"Settings.swipeForVideoPIP.Notice" = "Jeśli włączone, przesunięcie palcem otworzy film w trybie obrazu w obrazie."; + +"SessionBackup.Title" = "Kopia zapasowa kont"; +"SessionBackup.Sessions.Title" = "Sesje"; +"SessionBackup.Actions.Backup" = "Kopia zapasowa do pęku kluczy"; +"SessionBackup.Actions.Restore" = "Przywróć z pęku kluczy"; +"SessionBackup.Actions.DeleteAll" = "Usuń kopię zapasową pęku kluczy"; +"SessionBackup.Actions.DeleteOne" = "Usuń z kopii zapasowej"; +"SessionBackup.Actions.RemoveFromApp" = "Usuń z aplikacji"; +"SessionBackup.LastBackupAt" = "Ostatnia kopia zapasowa: %@"; +"SessionBackup.RestoreOK" = "OK. Przywrócono sesje: %@"; +"SessionBackup.LoggedIn" = "Zalogowano"; +"SessionBackup.LoggedOut" = "Wylogowano"; +"SessionBackup.DeleteAll.Title" = "Usunąć wszystkie sesje?"; +"SessionBackup.DeleteAll.Text" = "Wszystkie sesje zostaną usunięte z pęku kluczy.\n\nKonta nie zostaną wylogowane ze Swiftgram."; +"SessionBackup.DeleteSingle.Title" = "Usunąć 1 (jedną) sesję?"; +"SessionBackup.DeleteSingle.Text" = "%@ sesja zostanie usunięta z pęku kluczy.\n\nKonto nie zostanie wylogowane ze Swiftgram."; +"SessionBackup.RemoveFromApp.Title" = "Usunąć konto z aplikacji?"; +"SessionBackup.RemoveFromApp.Text" = "%@ sesja ZOSTANIE USUNIĘTA ze Swiftgram! Sesja pozostanie aktywna, więc możesz ją później przywrócić."; +"SessionBackup.Notice" = "Sesje są szyfrowane i przechowywane w pęku kluczy urządzenia. Sesje nigdy nie opuszczają urządzenia.\n\nWAŻNE: Aby przywrócić sesje na nowym urządzeniu lub po zresetowaniu systemu operacyjnego, MUSISZ włączyć szyfrowane kopie zapasowe, w przeciwnym razie pęk kluczy nie zostanie przeniesiony.\n\nUWAGA: Sesje mogą zostać unieważnione przez Telegram lub z innego urządzenia."; + +"MessageFilter.Title" = "Filtr wiadomości"; +"MessageFilter.SubTitle" = "Usuń elementy rozpraszające uwagę i zmniejsz widoczność wiadomości zawierających poniższe słowa kluczowe.\nW słowach kluczowych rozróżniana jest wielkość liter."; +"MessageFilter.Keywords.Title" = "Słowa kluczowe"; +"MessageFilter.InputPlaceholder" = "Wpisz słowo kluczowe"; + +"InputToolbar.Title" = "Panel formatowania"; + +"Notifications.MentionsAndReplies.Title" = "@Wzmianki i odpowiedzi"; +"Notifications.MentionsAndReplies.value.default" = "Domyślne"; +"Notifications.MentionsAndReplies.value.silenced" = "Wyciszone"; +"Notifications.MentionsAndReplies.value.disabled" = "Wyłączone"; +"Notifications.PinnedMessages.Title" = "Przypięte wiadomości"; +"Notifications.PinnedMessages.value.default" = "Domyślne"; +"Notifications.PinnedMessages.value.silenced" = "Wyciszone"; +"Notifications.PinnedMessages.value.disabled" = "Wyłączone"; + + +"PayWall.Text" = "Doładowany funkcjami Pro"; + +"PayWall.SessionBackup.Title" = "Kopia zapasowa sesji"; +"PayWall.SessionBackup.Notice" = "Przywróć sesje z zaszyfrowanej lokalnej kopii zapasowej pęku kluczy Apple."; +"PayWall.SessionBackup.Description" = "Zmiana urządzenia lub usunięcie Swiftgrama nie jest już problemem. Przywróć wszystkie sesje, które są nadal aktywne na serwerach Telegrama."; + +"PayWall.MessageFilter.Title" = "Filtr wiadomości"; +"PayWall.MessageFilter.Notice" = "Zmniejsz widoczność SPAM-u, promocji i irytujących wiadomości."; +"PayWall.MessageFilter.Description" = "Utwórz listę słów kluczowych, których nie chcesz często widzieć, a Swiftgram zredukuje liczbę elementów rozpraszających uwagę."; + +"PayWall.Notifications.Title" = "Wyłącz @wzmianki i odpowiedzi"; +"PayWall.Notifications.Notice" = "Ukryj lub wycisz nieistotne powiadomienia."; +"PayWall.Notifications.Description" = "Koniec z przypietymi wiadomościami i @wzmiankami, gdy potrzebujesz odrobiny spokoju."; + +"PayWall.InputToolbar.Title" = "Panel formatowania"; +"PayWall.InputToolbar.Notice" = "Oszczędź czas na przygotowywaniu wpisów dzięki panelowi tuż nad klawiaturą."; +"PayWall.InputToolbar.Description" = "Zastosuj i wyczyść formatowanie lub wstaw nowe wiersze jak profesjonalista."; + +"PayWall.AppIcons.Title" = "Unikalne ikony aplikacji"; +"PayWall.AppIcons.Notice" = "Dostosuj wygląd Swiftgram na ekranie głównym."; + +"PayWall.About.Title" = "O Swiftgram Pro"; +"PayWall.About.Notice" = "Bezpłatna wersja Swiftgram oferuje dziesiątki funkcji i ulepszeń w stosunku do aplikacji Telegram. Innowacje i synchronizacja Swiftgram z miesięcznymi aktualizacjami Telegram to ogromny wysiłek, który wymaga dużo czasu i drogiego sprzętu.\n\nSwiftgram to aplikacja typu open source, która szanuje twoją prywatność i nie przeszkadza ci reklamami. Subskrybując Swiftgram Pro uzyskujesz dostęp do ekskluzywnych funkcji i wspierasz niezależnego programistę."; +/* DO NOT TRANSLATE */ +"PayWall.About.Signature" = "@Kylmakalle"; +/* DO NOT TRANSLATE */ +"PayWall.About.SignatureURL" = "https://t.me/Kylmakalle"; + +"PayWall.ProSupport.Title" = "Problemy z płatnością?"; +"PayWall.ProSupport.Contact" = "Nie martw się!"; + +"PayWall.RestorePurchases" = "Przywróć zakup"; +"PayWall.Terms" = "Warunki korzystania z usługi"; +"PayWall.Privacy" = "Polityka prywatności"; +"PayWall.TermsURL" = "https://swiftgram.app/terms"; +"PayWall.PrivacyURL" = "https://swiftgram.app/privacy"; +"PayWall.Notice.Markdown" = "Subskrybując Swiftgram Pro akceptujesz [Warunki korzystania z usługi](%1$@) i [Politykę prywatności](%2$@) Swiftgram."; +"PayWall.Notice.Raw" = "Subskrybując Swiftgram Pro akceptujesz Warunki korzystania z usługi i Politykę prywatności Swiftgram."; + +"PayWall.Button.OpenPro" = "Używaj funkcji Pro"; +"PayWall.Button.Purchasing" = "Kupowanie…"; +"PayWall.Button.Restoring" = "Przywracanie zakupu…"; +"PayWall.Button.Validating" = "Weryfikacja zakupu…"; +"PayWall.Button.PaymentsUnavailable" = "Płatności niedostępne"; +"PayWall.Button.BuyInAppStore" = "Subskrybuj w wersji z App Store"; +"PayWall.Button.Subscribe" = "Subskrybuj za %@ / miesiąc"; +"PayWall.Button.ContactingAppStore" = "Kontakt z App Store…"; + +"Paywall.Error.Title" = "Błąd"; +"PayWall.ValidationError" = "Błąd weryfikacji"; +"PayWall.ValidationError.TryAgain" = "Coś poszło nie tak podczas weryfikacji zakupu. Nie martw się! Spróbuj przywrócić zakupy trochę później."; +"PayWall.ValidationError.Expired" = "Twoja subskrypcja wygasła. Subskrybuj ponownie, aby odzyskać dostęp do funkcji Pro."; diff --git a/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..94e181ea2ee --- /dev/null +++ b/Swiftgram/SGStrings/Strings/pt.lproj/SGLocalizable.strings @@ -0,0 +1,263 @@ +"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"; +"Settings.Translation.Backend" = "Serviço"; +/* Do not translate */ +"Settings.Translation.Backend.default" = "Telegram"; +/* Do not translate */ +"Settings.Translation.Backend.gtranslate" = "GTranslate"; +"Settings.Translation.Backend.system" = "Sistema"; +"Settings.Translation.Backend.Notice" = "O Swiftgram usará %@ caso o serviço de tradução selecionado não esteja disponível."; + +"Settings.Transcription.Header" = "VOZ PARA TEXTO"; +"Settings.Transcription.Backend" = "Serviço"; +/* Do not translate */ +"Settings.Transcription.Backend.default" = "Telegram"; +/* Do not translate */ +"Settings.Transcription.Backend.apple" = "Apple"; +"Settings.Transcription.Backend.Notice" = "O Swiftgram usará %@ caso o serviço de transcrição selecionado não esteja disponível."; + +"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 em HD"; +"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."; + +"AppBadge.Title" = "Emblema do Aplicativo"; +"AppBadge.Notice" = "Personalizar Emblema do Aplicativo Exibido nas capturas de tela"; diff --git a/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/ro.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..ccb2ad1e46f --- /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 00000000000..1dad777f9de --- /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 00000000000..869c70ba7ea --- /dev/null +++ b/Swiftgram/SGStrings/Strings/si.lproj/SGLocalizable.strings @@ -0,0 +1,2 @@ +"Settings.Tabs.Header" = "පටිති"; +"ContextMenu.SaveToCloud" = "මේඝයට සුරකින්න"; diff --git a/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..77376339e39 --- /dev/null +++ b/Swiftgram/SGStrings/Strings/sk.lproj/SGLocalizable.strings @@ -0,0 +1,4 @@ +"Settings.Tabs.Header" = "ZÁLOŽKY"; +"Settings.Tabs.ShowContacts" = "Zobraziť kontakty"; +"Settings.Tabs.ShowNames" = "Zobraziť názvy záložiek"; +"ContextMenu.SaveToCloud" = "Uložiť na Cloud"; diff --git a/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings b/Swiftgram/SGStrings/Strings/sr.lproj/SGLocalizable.strings new file mode 100644 index 00000000000..c71efa9f165 --- /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 00000000000..de9ed08295e --- /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 00000000000..7f1b643ec79 --- /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 00000000000..405fcfb869d --- /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 00000000000..cfab47bc31a --- /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 00000000000..8878463be82 --- /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 00000000000..0dfe9dba25f --- /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 00000000000..fd7effeb8c1 --- /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 00000000000..ed4f4a60819 --- /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 00000000000..94cf367af5c --- /dev/null +++ b/Swiftgram/SGSwiftSignalKit/Sources/SGSwiftSignalKit.swift @@ -0,0 +1,190 @@ +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() +public extension Signal { + 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() + } + ) + } + } + + func task() async throws -> T { + let disposable = MetaDisposable() + return try await withTaskCancellationHandler(operation: { + return try await withCheckedThrowingContinuation { continuation in + disposable.set((self |> take(1)).startStandalone( + next: { value in + continuation.resume(returning: value) + }, + error: { err in + continuation.resume(throwing: SignalError(err)) + } + )) + } + }, onCancel: { + disposable.dispose() + }) + } + + func stream() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + let disposable = self.startStandalone( + next: { value in + continuation.yield(value) + }, + error: { err in + continuation.finish(throwing: SignalError(err)) + }, + completed: { + continuation.finish() + } + ) + continuation.onTermination = { _ in + disposable.dispose() + } + } + } +} + +public extension Signal where E == NoError { + func task() async -> T { + return await self.get() + } + + func stream() -> AsyncStream { + return AsyncStream { continuation in + let disposable = self.startStandalone( + next: { value in + continuation.yield(value) + }, + completed: { + continuation.finish() + } + ) + continuation.onTermination = { _ in + disposable.dispose() + } + } + } +} + +// Extension for general Signal types - AsyncStream support +public extension Signal { + 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 +public extension Signal where E == NoError { + 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 00000000000..9437ba2d57d --- /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 00000000000..066174d8140 --- /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 00000000000..6beaa484985 --- /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 00000000000..8ff4cc57c15 --- /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 00000000000..70f7354e971 --- /dev/null +++ b/Swiftgram/SGTranslationLangFix/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGTranslationLangFix", + module_name = "SGTranslationLangFix", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift b/Swiftgram/SGTranslationLangFix/Sources/SGTranslationLangFix.swift new file mode 100644 index 00000000000..f308de08df3 --- /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 00000000000..1d581760f2d --- /dev/null +++ b/Swiftgram/SGWebAppExtensions/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebAppExtensions", + module_name = "SGWebAppExtensions", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift b/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift new file mode 100644 index 00000000000..355a5664c22 --- /dev/null +++ b/Swiftgram/SGWebAppExtensions/Sources/LocationHashParser.swift @@ -0,0 +1,58 @@ +import Foundation + +func urlSafeDecode(_ urlencoded: String) -> String { + return urlencoded.replacingOccurrences(of: "+", with: "%20").removingPercentEncoding ?? urlencoded +} + +public func urlParseHashParams(_ locationHash: String) -> [String: String?] { + var params = [String: String?]() + var localLocationHash = locationHash.removePrefix("#") // Remove leading '#' + + if localLocationHash.isEmpty { + return params + } + + if !localLocationHash.contains("=") && !localLocationHash.contains("?") { + params["_path"] = urlSafeDecode(localLocationHash) + return params + } + + let qIndex = localLocationHash.firstIndex(of: "?") + if let qIndex = qIndex { + let pathParam = String(localLocationHash[.. [String: String?] { + var params = [String: String?]() + + if queryString.isEmpty { + return params + } + + let queryStringParams = queryString.split(separator: "&") + for param in queryStringParams { + let parts = param.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + let paramName = urlSafeDecode(String(parts[0])) + let paramValue = parts.count > 1 ? urlSafeDecode(String(parts[1])) : nil + params[paramName] = paramValue + } + + return params +} + +extension String { + func removePrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } +} diff --git a/Swiftgram/SGWebSettings/BUILD b/Swiftgram/SGWebSettings/BUILD new file mode 100644 index 00000000000..ef1ee7626ad --- /dev/null +++ b/Swiftgram/SGWebSettings/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebSettings", + module_name = "SGWebSettings", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebSettings/Sources/File.swift b/Swiftgram/SGWebSettings/Sources/File.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Swiftgram/SGWebSettingsScheme/BUILD b/Swiftgram/SGWebSettingsScheme/BUILD new file mode 100644 index 00000000000..7bec1071410 --- /dev/null +++ b/Swiftgram/SGWebSettingsScheme/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGWebSettingsScheme", + module_name = "SGWebSettingsScheme", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) \ No newline at end of file diff --git a/Swiftgram/SGWebSettingsScheme/Sources/File.swift b/Swiftgram/SGWebSettingsScheme/Sources/File.swift new file mode 100644 index 00000000000..b8e976e99f7 --- /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 00000000000..a4eeb901eac --- /dev/null +++ b/Swiftgram/SwiftSoup/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftSoup", + module_name = "SwiftSoup", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SwiftSoup/Sources/ArrayExt.swift b/Swiftgram/SwiftSoup/Sources/ArrayExt.swift new file mode 100644 index 00000000000..a3b329f03d6 --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/ArrayExt.swift @@ -0,0 +1,21 @@ +// +// ArrayExt.swift +// SwifSoup +// +// Created by Nabil Chatbi on 05/10/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +extension Array where Element : Equatable { + func lastIndexOf(_ e: Element) -> Int { + for pos in (0.. String { + return key + } + + /** + Set the attribute key; case is preserved. + @param key the new key; must not be null + */ + open func setKey(key: String) throws { + try Validate.notEmpty(string: key) + self.key = key.trim() + } + + /** + Get the attribute value. + @return the attribute value + */ + open func getValue() -> String { + return value + } + + /** + Set the attribute value. + @param value the new attribute value; must not be null + */ + @discardableResult + open func setValue(value: String) -> String { + let old = self.value + self.value = value + return old + } + + /** + Get the HTML representation of this attribute; e.g. {@code href="index.html"}. + @return HTML + */ + public func html() -> String { + let accum = StringBuilder() + html(accum: accum, out: (Document("")).outputSettings()) + return accum.toString() + } + + public func html(accum: StringBuilder, out: OutputSettings ) { + accum.append(key) + if (!shouldCollapseAttribute(out: out)) { + accum.append("=\"") + Entities.escape(accum, value, out, true, false, false) + accum.append("\"") + } + } + + /** + Get the string representation of this attribute, implemented as {@link #html()}. + @return string + */ + open func toString() -> String { + return html() + } + + /** + * Create a new Attribute from an unencoded key and a HTML attribute encoded value. + * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars. + * @param encodedValue HTML attribute encoded value + * @return attribute + */ + public static func createFromEncoded(unencodedKey: String, encodedValue: String) throws ->Attribute { + let value = try Entities.unescape(string: encodedValue, strict: true) + return try Attribute(key: unencodedKey, value: value) + } + + public func isDataAttribute() -> Bool { + return key.startsWith(Attributes.dataPrefix) && key.count > Attributes.dataPrefix.count + } + + /** + * Collapsible if it's a boolean attribute and value is empty or same as name + * + * @param out Outputsettings + * @return Returns whether collapsible or not + */ + public final func shouldCollapseAttribute(out: OutputSettings) -> Bool { + return ("" == value || value.equalsIgnoreCase(string: key)) + && out.syntax() == OutputSettings.Syntax.html + && isBooleanAttribute() + } + + public func isBooleanAttribute() -> Bool { + return Attribute.booleanAttributes.contains(key.lowercased()) + } + + public func hashCode() -> Int { + var result = key.hashValue + result = 31 * result + value.hashValue + return result + } + + public func clone() -> Attribute { + do { + return try Attribute(key: key, value: value) + } catch Exception.Error( _, let msg) { + print(msg) + } catch { + + } + return try! Attribute(key: "", value: "") + } +} + +extension Attribute: Equatable { + static public func == (lhs: Attribute, rhs: Attribute) -> Bool { + return lhs.value == rhs.value && lhs.key == rhs.key + } + +} diff --git a/Swiftgram/SwiftSoup/Sources/Attributes.swift b/Swiftgram/SwiftSoup/Sources/Attributes.swift new file mode 100644 index 00000000000..2ffa006a80b --- /dev/null +++ b/Swiftgram/SwiftSoup/Sources/Attributes.swift @@ -0,0 +1,235 @@ +// +// Attributes.swift +// SwifSoup +// +// Created by Nabil Chatbi on 29/09/16. +// Copyright © 2016 Nabil Chatbi.. All rights reserved. +// + +import Foundation + +/** + * The attributes of an Element. + *

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

+ *

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

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

CssSelector syntax

+ *

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

+ *

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

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

Combinators

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

Pseudo selectors

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

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

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

Structural pseudo selectors

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

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

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

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

+ * + *

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

+ * + *

Elements used:

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

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

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

Elements used:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

One Two Three
Four

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * For example, given HTML {@code

Hello there now!

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

+ * For example, given HTML {@code

Hello there now!

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

}, would return + * {@code

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

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

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

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

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

This is SwiftSoup

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

This is SwiftSoup

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

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

+ * + * E.g. with HTML:

{@code

One Two
}

+ *

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

+ *

HTML = {@code

One Two
}

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

+ * E.g. HTML: {@code

Hello there

now

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

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

+ * E.g. HTML: {@code

Hello

there

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

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

+ * E.g. HTML: {@code

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

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

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

+ Start with one of the defaults: +

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

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

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

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

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

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

+

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

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

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

+

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

+

+ Does not allow images. +

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

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

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

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

+

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

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

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

+

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

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

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

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

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

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

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

+

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

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

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

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

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

+ * + */ +public class XmlTreeBuilder: TreeBuilder { + + public override init() { + super.init() + } + + public override func defaultSettings() -> ParseSettings { + return ParseSettings.preserveCase + } + + public func parse(_ input: String, _ baseUri: String)throws->Document { + return try parse(input, baseUri, ParseErrorList.noTracking(), ParseSettings.preserveCase) + } + + override public func initialiseParse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings) { + super.initialiseParse(input, baseUri, errors, settings) + stack.append(doc) // place the document onto the stack. differs from HtmlTreeBuilder (not on stack) + doc.outputSettings().syntax(syntax: OutputSettings.Syntax.xml) + } + + override public func process(_ token: Token)throws->Bool { + // start tag, end tag, doctype, comment, character, eof + switch (token.type) { + case .StartTag: + try insert(token.asStartTag()) + break + case .EndTag: + try popStackToClose(token.asEndTag()) + break + case .Comment: + try insert(token.asComment()) + break + case .Char: + try insert(token.asCharacter()) + break + case .Doctype: + try insert(token.asDoctype()) + break + case .EOF: // could put some normalisation here if desired + break +// default: +// try Validate.fail(msg: "Unexpected token type: " + token.tokenType()) + } + return true + } + + private func insertNode(_ node: Node)throws { + try currentElement()?.appendChild(node) + } + + @discardableResult + func insert(_ startTag: Token.StartTag)throws->Element { + let tag: Tag = try Tag.valueOf(startTag.name(), settings) + // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html. + let el: Element = try Element(tag, baseUri, settings.normalizeAttributes(startTag._attributes)) + try insertNode(el) + if (startTag.isSelfClosing()) { + tokeniser.acknowledgeSelfClosingFlag() + if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above. + { + tag.setSelfClosing() + } + } else { + stack.append(el) + } + return el + } + + func insert(_ commentToken: Token.Comment)throws { + let comment: Comment = Comment(commentToken.getData(), baseUri) + var insert: Node = comment + if (commentToken.bogus) { // xml declarations are emitted as bogus comments (which is right for html, but not xml) + // so we do a bit of a hack and parse the data as an element to pull the attributes out + let data: String = comment.getData() + if (data.count > 1 && (data.startsWith("!") || data.startsWith("?"))) { + let doc: Document = try SwiftSoup.parse("<" + data.substring(1, data.count - 2) + ">", baseUri, Parser.xmlParser()) + let el: Element = doc.child(0) + insert = XmlDeclaration(settings.normalizeTag(el.tagName()), comment.getBaseUri(), data.startsWith("!")) + insert.getAttributes()?.addAll(incoming: el.getAttributes()) + } + } + try insertNode(insert) + } + + func insert(_ characterToken: Token.Char)throws { + let node: Node = TextNode(characterToken.getData()!, baseUri) + try insertNode(node) + } + + func insert(_ d: Token.Doctype)throws { + let doctypeNode = DocumentType(settings.normalizeTag(d.getName()), d.getPubSysKey(), d.getPublicIdentifier(), d.getSystemIdentifier(), baseUri) + try insertNode(doctypeNode) + } + + /** + * If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not + * found, skips. + * + * @param endTag + */ + private func popStackToClose(_ endTag: Token.EndTag)throws { + let elName: String = try endTag.name() + var firstFound: Element? = nil + + for pos in (0..Array { + initialiseParse(inputFragment, baseUri, errors, settings) + try runParser() + return doc.getChildNodes() + } +} diff --git a/Swiftgram/Wrap/BUILD b/Swiftgram/Wrap/BUILD new file mode 100644 index 00000000000..2a1b4a85784 --- /dev/null +++ b/Swiftgram/Wrap/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Wrap", + module_name = "Wrap", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/Wrap/Sources/Wrap.swift b/Swiftgram/Wrap/Sources/Wrap.swift new file mode 100644 index 00000000000..055ab2b8754 --- /dev/null +++ b/Swiftgram/Wrap/Sources/Wrap.swift @@ -0,0 +1,568 @@ +/** + * Wrap - the easy to use Swift JSON encoder + * + * For usage, see documentation of the classes/symbols listed in this file, as well + * as the guide available at: github.com/johnsundell/wrap + * + * Copyright (c) 2015 - 2017 John Sundell. Licensed under the MIT license, as follows: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import Foundation + +/// Type alias defining what type of Dictionary that Wrap produces +public typealias WrappedDictionary = [String : Any] + +/** + * Wrap any object or value, encoding it into a JSON compatible Dictionary + * + * - Parameter object: The object to encode + * - Parameter context: An optional contextual object that will be available throughout + * the wrapping process. Can be used to inject extra information or objects needed to + * perform the wrapping. + * - Parameter dateFormatter: Optionally pass in a date formatter to use to encode any + * `NSDate` values found while encoding the object. If this is `nil`, any found date + * values will be encoded using the "yyyy-MM-dd HH:mm:ss" format. + * + * All the type's stored properties (both public & private) will be recursively + * encoded with their property names as the key. For example, given the following + * Struct as input: + * + * ``` + * struct User { + * let name = "John" + * let age = 28 + * } + * ``` + * + * This function will produce the following output: + * + * ``` + * [ + * "name" : "John", + * "age" : 28 + * ] + * ``` + * + * The object passed to this function must be an instance of a Class, or a value + * based on a Struct. Standard library values, such as Ints, Strings, etc are not + * valid input. + * + * Throws a WrapError if the operation could not be completed. + * + * For more customization options, make your type conform to `WrapCustomizable`, + * that lets you override encoding keys and/or the whole wrapping process. + * + * See also `WrappableKey` (for dictionary keys) and `WrappableEnum` for Enum values. + */ +public func wrap(_ object: T, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> WrappedDictionary { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, enableCustomizedWrapping: true) +} + +/** + * Alternative `wrap()` overload that returns JSON-based `Data` + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ object: T, writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: object, writingOptions: writingOptions ?? []) +} + +/** + * Alternative `wrap()` overload that encodes an array of objects into an array of dictionaries + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ objects: [T], context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> [WrappedDictionary] { + return try objects.map { try wrap($0, context: context, dateFormatter: dateFormatter) } +} + +/** + * Alternative `wrap()` overload that encodes an array of objects into JSON-based `Data` + * + * See the documentation for the dictionary-based `wrap()` function for more information + */ +public func wrap(_ objects: [T], writingOptions: JSONSerialization.WritingOptions? = nil, context: Any? = nil, dateFormatter: DateFormatter? = nil) throws -> Data { + let dictionaries: [WrappedDictionary] = try wrap(objects, context: context, dateFormatter: dateFormatter) + return try JSONSerialization.data(withJSONObject: dictionaries, options: writingOptions ?? []) +} + +// Enum describing various styles of keys in a wrapped dictionary +public enum WrapKeyStyle { + /// The keys in a dictionary produced by Wrap should match their property name (default) + case matchPropertyName + /// The keys in a dictionary produced by Wrap should be converted to snake_case. + /// For example, "myProperty" will be converted to "my_property". All keys will be lowercased. + case convertToSnakeCase +} + +/** + * Protocol providing the main customization point for Wrap + * + * It's optional to implement all of the methods in this protocol, as Wrap + * supplies default implementations of them. + */ +public protocol WrapCustomizable { + /** + * The style that wrap should apply to the keys of a wrapped dictionary + * + * The value of this property is ignored if a type provides a custom + * implementation of the `keyForWrapping(propertyNamed:)` method. + */ + var wrapKeyStyle: WrapKeyStyle { get } + /** + * Override the wrapping process for this type + * + * All top-level types should return a `WrappedDictionary` from this method. + * + * You may use the default wrapping implementation by using a `Wrapper`, but + * never call `wrap()` from an implementation of this method, since that might + * cause an infinite recursion. + * + * The context & dateFormatter passed to this method is any formatter that you + * supplied when initiating the wrapping process by calling `wrap()`. + * + * Returning nil from this method will be treated as an error, and cause + * a `WrapError.wrappingFailedForObject()` error to be thrown. + */ + func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? + /** + * Override the key that will be used when encoding a certain property + * + * Returning nil from this method will cause Wrap to skip the property + */ + func keyForWrapping(propertyNamed propertyName: String) -> String? + /** + * Override the wrapping of any property of this type + * + * The original value passed to this method will be the original value that the + * type is currently storing for the property. You can choose to either use this, + * or just access the property in question directly. + * + * The dateFormatter passed to this method is any formatter that you supplied + * when initiating the wrapping process by calling `wrap()`. + * + * Returning nil from this method will cause Wrap to use the default + * wrapping mechanism for the property, so you can choose which properties + * you want to customize the wrapping for. + * + * If you encounter an error while attempting to wrap the property in question, + * you can choose to throw. This will cause a WrapError.WrappingFailedForObject + * to be thrown from the main `wrap()` call that started the process. + */ + func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? +} + +/// Protocol implemented by types that may be used as keys in a wrapped Dictionary +public protocol WrappableKey { + /// Convert this type into a key that can be used in a wrapped Dictionary + func toWrappedKey() -> String +} + +/** + * Protocol implemented by Enums to enable them to be directly wrapped + * + * If an Enum implementing this protocol conforms to `RawRepresentable` (it's based + * on a raw type), no further implementation is required. If you wish to customize + * how the Enum is wrapped, you can use the APIs in `WrapCustomizable`. + */ +public protocol WrappableEnum: WrapCustomizable {} + +/// Protocol implemented by Date types to enable them to be wrapped +public protocol WrappableDate { + /// Wrap the date using a date formatter, generating a string representation + func wrap(dateFormatter: DateFormatter) -> String +} + +/** + * Class used to wrap an object or value. Use this in any custom `wrap()` implementations + * in case you only want to add on top of the default implementation. + * + * You normally don't have to interact with this API. Use the `wrap()` function instead + * to wrap an object from top-level code. + */ +public class Wrapper { + fileprivate let context: Any? + fileprivate var dateFormatter: DateFormatter? + + /** + * Initialize an instance of this class + * + * - Parameter context: An optional contextual object that will be available throughout the + * wrapping process. Can be used to inject extra information or objects needed to perform + * the wrapping. + * - Parameter dateFormatter: Any specific date formatter to use to encode any found `NSDate` + * values. If this is `nil`, any found date values will be encoded using the "yyyy-MM-dd + * HH:mm:ss" format. + */ + public init(context: Any? = nil, dateFormatter: DateFormatter? = nil) { + self.context = context + self.dateFormatter = dateFormatter + } + + /// Perform automatic wrapping of an object or value. For more information, see `Wrap()`. + public func wrap(object: Any) throws -> WrappedDictionary { + return try self.wrap(object: object, enableCustomizedWrapping: false) + } +} + +/// Error type used by Wrap +public enum WrapError: Error { + /// Thrown when an invalid top level object (such as a String or Int) was passed to `Wrap()` + case invalidTopLevelObject(Any) + /// Thrown when an object couldn't be wrapped. This is a last resort error. + case wrappingFailedForObject(Any) +} + +// MARK: - Default protocol implementations + +/// Extension containing default implementations of `WrapCustomizable`. Override as you see fit. +public extension WrapCustomizable { + var wrapKeyStyle: WrapKeyStyle { + return .matchPropertyName + } + + func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) + } + + func keyForWrapping(propertyNamed propertyName: String) -> String? { + switch self.wrapKeyStyle { + case .matchPropertyName: + return propertyName + case .convertToSnakeCase: + return self.convertPropertyNameToSnakeCase(propertyName: propertyName) + } + } + + func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { + return try Wrapper(context: context, dateFormatter: dateFormatter).wrap(value: originalValue, propertyName: propertyName) + } +} + +/// Extension adding convenience APIs to `WrapCustomizable` types +public extension WrapCustomizable { + /// Convert a given property name (assumed to be camelCased) to snake_case + func convertPropertyNameToSnakeCase(propertyName: String) -> String { + let regex = try! NSRegularExpression(pattern: "(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])", options: []) + let range = NSRange(location: 0, length: propertyName.count) + let camelCasePropertyName = regex.stringByReplacingMatches(in: propertyName, options: [], range: range, withTemplate: "_$1$2") + return camelCasePropertyName.lowercased() + } +} + +/// Extension providing a default wrapping implementation for `RawRepresentable` Enums +public extension WrappableEnum where Self: RawRepresentable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.rawValue + } +} + +/// Extension customizing how Arrays are wrapped +extension Array: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) + } +} + +/// Extension customizing how Dictionaries are wrapped +extension Dictionary: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self) + } +} + +/// Extension customizing how Sets are wrapped +extension Set: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: self) + } +} + +/// Extension customizing how Int64s are wrapped, ensuring compatbility with 32 bit systems +extension Int64: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return NSNumber(value: self) + } +} + +/// Extension customizing how UInt64s are wrapped, ensuring compatbility with 32 bit systems +extension UInt64: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return NSNumber(value: self) + } +} + +/// Extension customizing how NSStrings are wrapped +extension NSString: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self + } +} + +/// Extension customizing how NSURLs are wrapped +extension NSURL: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.absoluteString + } +} + +/// Extension customizing how URLs are wrapped +extension URL: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return self.absoluteString + } +} + + +/// Extension customizing how NSArrays are wrapped +extension NSArray: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(collection: Array(self)) + } +} + +#if !os(Linux) +/// Extension customizing how NSDictionaries are wrapped +extension NSDictionary: WrapCustomizable { + public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { + return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(dictionary: self as [NSObject : AnyObject]) + } +} +#endif + +/// Extension making Int a WrappableKey +extension Int: WrappableKey { + public func toWrappedKey() -> String { + return String(self) + } +} + +/// Extension making Date a WrappableDate +extension Date: WrappableDate { + public func wrap(dateFormatter: DateFormatter) -> String { + return dateFormatter.string(from: self) + } +} + +#if !os(Linux) +/// Extension making NSdate a WrappableDate +extension NSDate: WrappableDate { + public func wrap(dateFormatter: DateFormatter) -> String { + return dateFormatter.string(from: self as Date) + } +} +#endif + +// MARK: - Private + +private extension Wrapper { + func wrap(object: T, enableCustomizedWrapping: Bool) throws -> WrappedDictionary { + if enableCustomizedWrapping { + if let customizable = object as? WrapCustomizable { + let wrapped = try self.performCustomWrapping(object: customizable) + + guard let wrappedDictionary = wrapped as? WrappedDictionary else { + throw WrapError.invalidTopLevelObject(object) + } + + return wrappedDictionary + } + } + + var mirrors = [Mirror]() + var currentMirror: Mirror? = Mirror(reflecting: object) + + while let mirror = currentMirror { + mirrors.append(mirror) + currentMirror = mirror.superclassMirror + } + + return try self.performWrapping(object: object, mirrors: mirrors.reversed()) + } + + func wrap(object: T, writingOptions: JSONSerialization.WritingOptions) throws -> Data { + let dictionary = try self.wrap(object: object, enableCustomizedWrapping: true) + return try JSONSerialization.data(withJSONObject: dictionary, options: writingOptions) + } + + func wrap(value: T, propertyName: String? = nil) throws -> Any? { + if let customizable = value as? WrapCustomizable { + return try self.performCustomWrapping(object: customizable) + } + + if let date = value as? WrappableDate { + return self.wrap(date: date) + } + + let mirror = Mirror(reflecting: value) + + if mirror.children.isEmpty { + if let displayStyle = mirror.displayStyle { + switch displayStyle { + case .enum: + if let wrappableEnum = value as? WrappableEnum { + if let wrapped = wrappableEnum.wrap(context: self.context, dateFormatter: self.dateFormatter) { + return wrapped + } + + throw WrapError.wrappingFailedForObject(value) + } + + return "\(value)" + case .struct: + return [:] + default: + return value + } + } + + if !(value is CustomStringConvertible) { + if String(describing: value) == "(Function)" { + return nil + } + } + + return value + } else if value is ExpressibleByNilLiteral && mirror.children.count == 1 { + if let firstMirrorChild = mirror.children.first { + return try self.wrap(value: firstMirrorChild.value, propertyName: propertyName) + } + } + + return try self.wrap(object: value, enableCustomizedWrapping: false) + } + + func wrap(collection: T) throws -> [Any] { + var wrappedArray = [Any]() + let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) + + for element in collection { + if let wrapped = try wrapper.wrap(value: element) { + wrappedArray.append(wrapped) + } + } + + return wrappedArray + } + + func wrap(dictionary: [K : V]) throws -> WrappedDictionary { + var wrappedDictionary = WrappedDictionary() + let wrapper = Wrapper(context: self.context, dateFormatter: self.dateFormatter) + + for (key, value) in dictionary { + let wrappedKey: String? + + if let stringKey = key as? String { + wrappedKey = stringKey + } else if let wrappableKey = key as? WrappableKey { + wrappedKey = wrappableKey.toWrappedKey() + } else if let stringConvertible = key as? CustomStringConvertible { + wrappedKey = stringConvertible.description + } else { + wrappedKey = nil + } + + if let wrappedKey = wrappedKey { + wrappedDictionary[wrappedKey] = try wrapper.wrap(value: value, propertyName: wrappedKey) + } + } + + return wrappedDictionary + } + + func wrap(date: WrappableDate) -> String { + let dateFormatter: DateFormatter + + if let existingFormatter = self.dateFormatter { + dateFormatter = existingFormatter + } else { + dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + self.dateFormatter = dateFormatter + } + + return date.wrap(dateFormatter: dateFormatter) + } + + func performWrapping(object: T, mirrors: [Mirror]) throws -> WrappedDictionary { + let customizable = object as? WrapCustomizable + var wrappedDictionary = WrappedDictionary() + + for mirror in mirrors { + for property in mirror.children { + + if (property.value as? WrapOptional)?.isNil == true { + continue + } + + guard let propertyName = property.label else { + continue + } + + let wrappingKey: String? + + if let customizable = customizable { + wrappingKey = customizable.keyForWrapping(propertyNamed: propertyName) + } else { + wrappingKey = propertyName + } + + if let wrappingKey = wrappingKey { + if let wrappedProperty = try customizable?.wrap(propertyNamed: propertyName, originalValue: property.value, context: self.context, dateFormatter: self.dateFormatter) { + wrappedDictionary[wrappingKey] = wrappedProperty + } else { + wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) + } + } + } + } + + return wrappedDictionary + } + + func performCustomWrapping(object: WrapCustomizable) throws -> Any { + guard let wrapped = object.wrap(context: self.context, dateFormatter: self.dateFormatter) else { + throw WrapError.wrappingFailedForObject(object) + } + + return wrapped + } +} + +// MARK: - Nil Handling + +private protocol WrapOptional { + var isNil: Bool { get } +} + +extension Optional : WrapOptional { + var isNil: Bool { + switch self { + case .none: + return true + case .some(let wrapped): + if let nillable = wrapped as? WrapOptional { + return nillable.isNil + } + return false + } + } +} \ No newline at end of file diff --git a/Telegram/BUILD b/Telegram/BUILD index af18262d331..19369e6c9c0 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -12,6 +12,12 @@ load("@build_bazel_rules_apple//apple:ios.bzl", "ios_ui_test", ) +# MARK: Swiftgram +load("@build_bazel_rules_apple//apple:watchos.bzl", + "watchos_application", + "watchos_extension", +) + load("@build_bazel_rules_apple//apple:resources.bzl", "swift_intent_library", ) @@ -136,9 +142,14 @@ genrule( "GeneratedPresentationStrings/Sources/PresentationStrings.m", "GeneratedPresentationStrings/Resources/PresentationStrings.data", ], + # MARK: Swiftgram + visibility = [ + "//visibility:public", + ], ) minimum_os_version = "13.0" +minimum_watchos_version="7.0" # MARK: Swiftgram notificationServiceExtensionVersion = "v1" @@ -247,9 +258,20 @@ 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",], +) + +# MARK: Swiftgram +filegroup( + name = "WatchAppStringResources", + srcs = glob([ + "Telegram-iOS/*.lproj/Localizable.strings", + ], exclude = ["Telegram-iOS/*.lproj/**/.*"]) + ["//Swiftgram/SGStrings:SGLocalizableStrings"], ) filegroup( @@ -273,13 +295,6 @@ filegroup( ], exclude = ["Telegram-iOS/Icons.xcassets/**/.*"]), ) -filegroup( - name = "AppIcons", - srcs = glob([ - "Telegram-iOS/AppIcons.xcassets/**/*", - ], exclude = ["Telegram-iOS/AppIcons.xcassets/**/.*"]), -) - filegroup( name = "DefaultAppIcon", srcs = glob([ @@ -287,26 +302,25 @@ filegroup( ], exclude = ["Telegram-iOS/DefaultAppIcon.xcassets/**/.*"]), ) -filegroup( - name = "DefaultIcon", - srcs = glob([ - "Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/*.png", - ]), -) - +# 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" ] [ @@ -332,12 +346,14 @@ objc_library( ], ) +SGRESOURCES = ["//Swiftgram/SGSettingsUI:SGUIAssets", "//Swiftgram/SGPayWall:SGPayWallAssets", "//Swiftgram/SGAppBadgeAssets:SGAppBadgeAssets"] + swift_library( name = "Lib", srcs = glob([ "Telegram-iOS/Application.swift", ]), - data = [ + data = SGRESOURCES + [ ":Icons", ":AppResources", ":AppIntentVocabularyResources", @@ -398,6 +414,16 @@ plist_fragment( tonsite + + CFBundleTypeRole + Viewer + CFBundleURLName + {telegram_bundle_id}.custom + CFBundleURLSchemes + + sg + + """.format( telegram_bundle_id = telegram_bundle_id, @@ -456,7 +482,7 @@ official_carplay_fragment = """ com.apple.developer.carplay-messaging """ -carplay_fragment = official_carplay_fragment if telegram_bundle_id in official_bundle_ids else "" +carplay_fragment = official_carplay_fragment # MARK: Swiftgram if telegram_bundle_id in official_bundle_ids else "" icloud_fragment = "" if not telegram_enable_icloud else """ com.apple.developer.icloud-services @@ -484,6 +510,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 """ @@ -513,7 +540,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 @@ -523,6 +550,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", @@ -537,9 +571,68 @@ plist_fragment( carplay_fragment, communication_notifications_fragment, signin_fragment, + # content_analysis ]) ) +# MARK: Swiftgram +filegroup( + name = "TelegramWatchExtensionResources", + srcs = glob([ + "Watch/Extension/Resources/**/*", + ], exclude = ["Watch/Extension/Resources/**/.*"]), +) + +filegroup( + name = "TelegramWatchAppResources", + srcs = glob([ + "Watch/Extension/Resources/**/*.png", + ], exclude = ["Watch/Extension/Resources/**/.*"]), +) + +filegroup( + name = "TelegramWatchAppAssets", + srcs = glob([ + "Watch/App/Assets.xcassets/**/*", + ], exclude = ["Watch/App/Assets.xcassets/**/.*"]), +) + +filegroup( + name = "TelegramWatchAppInterface", + srcs = glob([ + "Watch/App/Base.lproj/Interface.storyboard", + ]), +) + +objc_library( + name = "TelegramWatchLib", + srcs = glob([ + "Watch/Extension/**/*.m", + "Watch/Bridge/**/*.m", + "Watch/WatchCommonWatch/**/*.m", + "Watch/App/**/*.m", + "Watch/Extension/**/*.h", + "Watch/Bridge/**/*.h", + "Watch/WatchCommonWatch/**/*.h", + ]), + copts = [ + "-DTARGET_OS_WATCH=1", + "-ITelegram/Watch", + "-ITelegram/Watch/Extension", + "-ITelegram/Watch/Bridge", + "-ITelegram/Watch/App", + ], + sdk_frameworks = [ + "WatchKit", + "WatchConnectivity", + "ClockKit", + "UserNotifications", + "CoreLocation", + "CoreGraphics", + ], +) +# + plist_fragment( name = "VersionInfoPlist", extension = "plist", @@ -568,10 +661,87 @@ plist_fragment( template = """ CFBundleDisplayName - Telegram + Swiftgram """ ) +# MARK: Swiftgram +plist_fragment( + name = "WatchAppCompanionInfoPlist", + extension = "plist", + template = + """ + WKCompanionAppBundleIdentifier + {telegram_bundle_id} + """.format( + telegram_bundle_id = telegram_bundle_id, + ) +) + +plist_fragment( + name = "WatchAppInfoPlist", + extension = "plist", + template = + """ + CFBundleDevelopmentRegion + en + CFBundleIdentifier + {telegram_bundle_id}.watchkitapp + CFBundleName + Swiftgram + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + CFBundlePackageType + APPL + WKApplication + + WKCompanionAppBundleIdentifier + {telegram_bundle_id} + PrincipalClass + TGExtensionDelegate + """.format( + telegram_bundle_id = telegram_bundle_id, + ) +) + +watchos_application( + name = "TelegramWatchApp", + bundle_id = "{telegram_bundle_id}.watchkitapp".format( + telegram_bundle_id = telegram_bundle_id, + ), + bundle_name = "SwiftgramWatch", + infoplists = [ + ":WatchAppInfoPlist", + ":VersionInfoPlist", + ":BuildNumberInfoPlist", + ":AppNameInfoPlist", + ":WatchAppCompanionInfoPlist", + ], + minimum_os_version = minimum_watchos_version, + provisioning_profile = select({ + ":disableProvisioningProfilesSetting": None, + "//conditions:default": "@build_configuration//provisioning:WatchApp.mobileprovision", + }), + resources = [ + ":TelegramWatchAppResources", + ":TelegramWatchAppAssets", + ":TelegramWatchExtensionResources", + ], + storyboards = [ + ":TelegramWatchAppInterface", + ], + strings = [ + ":WatchAppStringResources", + ], + deps = [ + ":TelegramWatchLib", + ], +) +# + plist_fragment( name = "MtProtoKitInfoPlist", extension = "plist", @@ -806,6 +976,14 @@ genrule( cmd_bash = """ echo 'for f in $$1/*.framework; do binary_name=`echo $$(basename $$f) | sed -e "s/\\\\.framework//"`; strip -ST $$f/$$binary_name; done;' > $(location StripFramework.sh) + # MARK: Swiftgram + echo 'find "$$1" -type f \\( -perm +111 -o -name "*.dylib" \\) | while read -r bin; do \ + if otool -L "$$bin" | grep -q "/usr/lib/swift/libswift_Concurrency.dylib"; then \ + echo "Patching concurrency backport in: $$bin"; \ + chmod +w "$$(dirname $$bin)/"; \ + install_name_tool -change /usr/lib/swift/libswift_Concurrency.dylib @rpath/libswift_Concurrency.dylib "$$bin"; \ + fi; \ + done;' >> $(location StripFramework.sh) echo '' >> $(location StripFramework.sh) """, outs = [ @@ -901,7 +1079,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Share CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -993,7 +1171,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationContent CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1100,7 +1278,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.Widget CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1213,7 +1391,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.SiriIntents CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1334,6 +1512,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", @@ -1344,7 +1663,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.BroadcastUpload CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1438,7 +1757,7 @@ plist_fragment( CFBundleIdentifier {telegram_bundle_id}.NotificationService CFBundleName - Telegram + Swiftgram CFBundlePackageType XPC! NSExtension @@ -1506,11 +1825,11 @@ plist_fragment( CFBundleDevelopmentRegion en CFBundleDisplayName - Telegram + Swiftgram CFBundleIdentifier {telegram_bundle_id} CFBundleName - Telegram + Swiftgram CFBundlePackageType APPL CFBundleSignature @@ -1564,17 +1883,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 @@ -1681,7 +2000,7 @@ xcode_provisioning_profile( ) ios_application( - name = "Telegram", + name = "Swiftgram", bundle_id = "{telegram_bundle_id}".format( telegram_bundle_id = telegram_bundle_id, ), @@ -1716,9 +2035,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, @@ -1727,6 +2049,7 @@ ios_application( ":BroadcastUploadExtension", ], }), + watch_application = ":TelegramWatchApp", # MARK: Swiftgram deps = [ ":Main", ":Lib", @@ -1737,11 +2060,11 @@ ios_application( xcodeproj( name = "Telegram_xcodeproj", 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 6327b76b926..fa2ec92f8b3 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 deb4e1a3e95..fa82c25a643 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 @@ -1289,7 +1340,7 @@ private final class NotificationServiceHandler { } case let .groupCall(groupCallData): if let stateManager = strongSelf.stateManager { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) let _ = (stateManager.postbox.transaction { transaction -> TelegramUser? in @@ -1310,7 +1361,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 @@ -1319,7 +1370,7 @@ private final class NotificationServiceHandler { completed() }) } else { - var content = NotificationContent(isLockedMessage: nil) + var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) if let peer = fromPeer { 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 @@ -1745,7 +1801,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") } @@ -1808,7 +1864,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 @@ -2008,7 +2064,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)) @@ -2100,7 +2156,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) } @@ -2142,7 +2198,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) @@ -2194,7 +2250,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) } @@ -2235,7 +2291,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() @@ -2254,7 +2310,7 @@ private final class NotificationServiceHandler { }) } } else { - let content = NotificationContent(isLockedMessage: nil) + let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil) updateCurrentContent(content) completed() @@ -2288,11 +2344,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 @@ -2323,7 +2438,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) } @@ -2350,3 +2470,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/PatchMinOSVersion.source.sh b/Telegram/PatchMinOSVersion.source.sh index dd0607c7844..e2450ff63cb 100644 --- a/Telegram/PatchMinOSVersion.source.sh +++ b/Telegram/PatchMinOSVersion.source.sh @@ -13,3 +13,12 @@ if [ "$version" == "14.0" ]; then binary_path="$f/$(basename $f | sed -e s/\.appex//g)" xcrun lipo "$binary_path" -remove armv7 -o "$binary_path" 2>/dev/null || true fi + +# MARK: Swiftgram +find "$1" -type f \( -perm +111 -o -name "*.dylib" \) | while read -r bin; do + if otool -L "$bin" | grep -q "/usr/lib/swift/libswift_Concurrency.dylib"; then + echo "Patching concurrency backport in: $bin" + chmod +w "$(dirname $bin)/" + install_name_tool -change /usr/lib/swift/libswift_Concurrency.dylib @rpath/libswift_Concurrency.dylib "$bin" + fi +done \ No newline at end of file diff --git a/Telegram/SGActionRequestHandler/Action.js b/Telegram/SGActionRequestHandler/Action.js new file mode 100644 index 00000000000..11832ae69cf --- /dev/null +++ b/Telegram/SGActionRequestHandler/Action.js @@ -0,0 +1,21 @@ +var Action = function() {}; + +Action.prototype = { + run: function(arguments) { + var payload = { + "url": document.documentURI + } + arguments.completionFunction(payload) + }, + finalize: function(arguments) { + const alertMessage = arguments["alert"] + const openURL = arguments["openURL"] + if (alertMessage) { + alert(alertMessage) + } else if (openURL) { + window.location = openURL + } + } +}; + +var ExtensionPreprocessingJS = new Action diff --git a/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json b/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/Telegram/SGActionRequestHandler/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json b/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json new file mode 100644 index 00000000000..94a9fc21819 --- /dev/null +++ b/Telegram/SGActionRequestHandler/Media.xcassets/TouchBarBezel.colorset/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "mac", + "color" : { + "reference" : "systemPurpleColor" + } + } + ] +} \ No newline at end of file diff --git a/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift b/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift new file mode 100644 index 00000000000..31ccdff0215 --- /dev/null +++ b/Telegram/SGActionRequestHandler/SGActionRequestHandler.swift @@ -0,0 +1,62 @@ +// import UIKit +// import MobileCoreServices +// import UrlEscaping + +// @objc(SGActionRequestHandler) +// class SGActionRequestHandler: NSObject, NSExtensionRequestHandling { +// var extensionContext: NSExtensionContext? + +// func beginRequest(with context: NSExtensionContext) { +// // Do not call super in an Action extension with no user interface +// self.extensionContext = context + +// let itemProvider = context.inputItems +// .compactMap({ $0 as? NSExtensionItem }) +// .reduce([NSItemProvider](), { partialResult, acc in +// var nextResult = partialResult +// nextResult += acc.attachments ?? [] +// return nextResult +// }) +// .filter({ $0.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) }) +// .first + +// guard let itemProvider = itemProvider else { +// return doneWithInvalidLink() +// } + +// itemProvider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { [weak self] item, error in +// DispatchQueue.main.async { +// guard +// let dictionary = item as? NSDictionary, +// let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary +// else { +// self?.doneWithInvalidLink() +// return +// } + +// if let url = results["url"] as? String, let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) { +// self?.doneWithResults(["openURL": "sg://parseurl?url=\(escapedUrl)"]) +// } else { +// self?.doneWithInvalidLink() +// } +// } +// }) +// } + +// func doneWithInvalidLink() { +// doneWithResults(["alert": "Invalid link"]) +// } + +// func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { +// if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { +// let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] +// let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: kUTTypePropertyList as String) +// let resultsItem = NSExtensionItem() +// resultsItem.attachments = [resultsProvider] +// self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) +// } else { +// self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) +// } +// self.extensionContext = nil +// } +// } diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json deleted file mode 100644 index 3364b2ef961..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon4@40x40-2.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon4@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon4@58x58-2.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon4@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon4@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon4@120x120-1.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon4@120x120.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon4@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon4@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon4@40x40.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon4@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon4@58x58.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon4@40x40-1.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon4@80x80.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon4@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon4@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon4@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png deleted file mode 100644 index 7169c854c35..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png deleted file mode 100644 index 7169c854c35..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png deleted file mode 100644 index 1529bf21a23..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png deleted file mode 100644 index 90e1de1ecfd..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png deleted file mode 100644 index d905a092330..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png deleted file mode 100644 index f7ed065d316..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png deleted file mode 100644 index 20070867ec3..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png deleted file mode 100644 index 39eec67f831..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png deleted file mode 100644 index 74aaa26f789..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png deleted file mode 100644 index 74aaa26f789..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png deleted file mode 100644 index 8d3559c8ffb..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png deleted file mode 100644 index 63146351553..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png deleted file mode 100644 index 2948e25763c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png deleted file mode 100644 index 2948e25763c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png deleted file mode 100644 index 5176f7d26c9..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackFilledIcon.appiconset/Icon4@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json deleted file mode 100644 index 8497b5a0d59..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon2@40x40.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon2@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon2@58x58.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon2@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon2@80x80.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon2@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon2@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon2@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon2@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon2@40x40-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon2@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon2@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon2@40x40-2.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon2@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon2@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon2@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon2@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png deleted file mode 100644 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png deleted file mode 100644 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png deleted file mode 100644 index 8044873c25c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png deleted file mode 100644 index bd9821af487..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png deleted file mode 100644 index a1d6016afb5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png deleted file mode 100644 index 090c237445f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png deleted file mode 100644 index 58f01e4c423..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png deleted file mode 100644 index fc834e964f1..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png deleted file mode 100644 index e311513f498..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png deleted file mode 100644 index e311513f498..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png deleted file mode 100644 index d7e2100fda9..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png deleted file mode 100644 index fb36db9ebaa..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png deleted file mode 100644 index b327187568f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png deleted file mode 100644 index b327187568f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png deleted file mode 100644 index 7a1aec12714..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlackIcon.appiconset/Icon2@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json deleted file mode 100644 index 021eed91bff..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon3@40x40.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon3@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon3@58x58.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon3@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon3@80x80.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon3@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon3@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon3@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon3@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon3@40x40-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon3@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon3@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon3@40x40-2.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon3@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon3@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon3@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon3@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png deleted file mode 100644 index 9c5ca6a0cf8..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png deleted file mode 100644 index 9c5ca6a0cf8..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png deleted file mode 100644 index de9fce9981d..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png deleted file mode 100644 index fb761143f01..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png deleted file mode 100644 index a09fd70b81d..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png deleted file mode 100644 index d5409e8ef14..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png deleted file mode 100644 index f9cf8bf6950..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png deleted file mode 100644 index 7004cb5a773..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png deleted file mode 100644 index 8b5050f6238..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png deleted file mode 100644 index 8b5050f6238..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png deleted file mode 100644 index dfea84b1b2c..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png deleted file mode 100644 index dfba84e32f0..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png deleted file mode 100644 index 3f1f9d34ee5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png deleted file mode 100644 index 3f1f9d34ee5..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png deleted file mode 100644 index 3c350f16499..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueFilledIcon.appiconset/Icon3@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json deleted file mode 100644 index 5315597fb05..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Contents.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon1@40x40-2.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon1@60x60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon1@58x58-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon1@87x87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon1@80x80-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon1@120x120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon1@120x120-1.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon1@180x180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon1@20x20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon1@40x40.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon1@29x29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon1@58x58.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon1@40x40-1.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon1@80x80.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon1@76x76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon1@152x152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon1@167x167.png", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@120x120.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png deleted file mode 100644 index d71dcd205e7..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@152x152.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png deleted file mode 100644 index f51ae17df90..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@167x167.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png deleted file mode 100644 index facbf49ff3a..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@180x180.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png deleted file mode 100644 index e865e6256b4..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@20x20.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png deleted file mode 100644 index 4865bb8b078..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40-2.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@40x40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png deleted file mode 100644 index ffae9ee7b74..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@60x60.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png deleted file mode 100644 index 07de560340f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@76x76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png b/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png deleted file mode 100644 index 95b278c284f..00000000000 Binary files a/Telegram/Telegram-iOS/AppIcons.xcassets/BlueIcon.appiconset/Icon1@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json b/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json deleted file mode 100644 index da4a164c918..00000000000 --- a/Telegram/Telegram-iOS/AppIcons.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png deleted file mode 100755 index 093f5821a54..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png deleted file mode 100755 index 13f8fe26949..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png deleted file mode 100755 index 46593ec4658..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png deleted file mode 100755 index ed0216f9318..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png deleted file mode 100755 index 1fcc6fc9bbf..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png deleted file mode 100644 index 20fe7d5eefc..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png deleted file mode 100644 index fafc0e385e6..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png deleted file mode 100644 index f00e3e2d617..00000000000 Binary files a/Telegram/Telegram-iOS/BlackClassicIcon.alticon/BlackClassicNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png deleted file mode 100755 index a327546043e..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png deleted file mode 100755 index a3972adecaa..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png deleted file mode 100644 index d86fb7cb550..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png deleted file mode 100755 index 0b52118b1c0..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png deleted file mode 100644 index 90e1de1ecfd..00000000000 Binary files a/Telegram/Telegram-iOS/BlackFilledIcon.alticon/BlackFilledIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png deleted file mode 100755 index 5a3a76cbdd7..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png deleted file mode 100755 index a1d6016afb5..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png deleted file mode 100755 index fb36db9ebaa..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png deleted file mode 100755 index 8044873c25c..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png deleted file mode 100755 index bd9821af487..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png deleted file mode 100755 index 55ae148ed83..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png deleted file mode 100755 index 638b30f339a..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png deleted file mode 100755 index 8b28ed057bc..00000000000 Binary files a/Telegram/Telegram-iOS/BlackIcon.alticon/BlackNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png deleted file mode 100755 index aa3ec282ce5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png deleted file mode 100755 index eca037efcf9..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png deleted file mode 100755 index 2e5e919205f..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png deleted file mode 100755 index 08da0b799af..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png deleted file mode 100755 index 342e2766d98..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png deleted file mode 100644 index b8befb1c7b3..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png deleted file mode 100644 index aa84350de54..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png deleted file mode 100644 index 32683874aa5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueClassicIcon.alticon/BlueClassicNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png deleted file mode 100755 index 7c851299aa5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png deleted file mode 100755 index 49b7fd968c7..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png deleted file mode 100644 index dfba84e32f0..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png deleted file mode 100644 index de9fce9981d..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png deleted file mode 100644 index fb761143f01..00000000000 Binary files a/Telegram/Telegram-iOS/BlueFilledIcon.alticon/BlueFilledIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png deleted file mode 100755 index 2e502e7dab1..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png deleted file mode 100755 index c47aeed4b14..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png deleted file mode 100755 index f07ad9568b3..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png deleted file mode 100755 index 1b21e8d9280..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png deleted file mode 100755 index 9bf363744d5..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png deleted file mode 100755 index dc5916282e1..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png deleted file mode 100755 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png b/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png deleted file mode 100755 index f7725e9914c..00000000000 Binary files a/Telegram/Telegram-iOS/BlueIcon.alticon/BlueNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png deleted file mode 100644 index dd360d8f50d..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png deleted file mode 100644 index 2e502e7dab1..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png deleted file mode 100644 index c47aeed4b14..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png deleted file mode 100644 index 6d9e7ab98c7..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png deleted file mode 100644 index 9bf363744d5..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueIconLargeIpad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png deleted file mode 100644 index dc5916282e1..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png deleted file mode 100644 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png deleted file mode 100644 index 0898af42d99..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png deleted file mode 100644 index f7725e9914c..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/BlueNotificationIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json index 4d65457087b..b45cfedbdcd 100644 --- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json +++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Contents.json @@ -1,110 +1,9 @@ { "images" : [ { - "filename" : "BlueNotificationIcon@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "BlueNotificationIcon@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "Simple@58x58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Simple@87x87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "Simple@80x80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "BlueIcon@2x-1.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "BlueIcon@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "BlueIcon@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "BlueNotificationIcon.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "BlueNotificationIcon@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "Simple@29x29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "Simple@58x58-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Simple@40x40-1.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "Simple@80x80-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "BlueIconIpad@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "BlueIconLargeIpad@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "Simple-iTunesArtwork.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "Swiftgram.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png deleted file mode 100644 index f00a2857f0f..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple-iTunesArtwork.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png deleted file mode 100644 index 90d7b67bc03..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png deleted file mode 100644 index a79cb5dcdc0..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@40x40-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png deleted file mode 100644 index aa6a4a442ec..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png deleted file mode 100644 index aa6a4a442ec..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png deleted file mode 100644 index 385bc474b20..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80-1.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png deleted file mode 100644 index 385bc474b20..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png deleted file mode 100644 index c0a9ce93195..00000000000 Binary files a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Simple@87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png new file mode 100644 index 00000000000..a28a393d1ec Binary files /dev/null and b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/AppIconLLC.appiconset/Swiftgram.png differ diff --git a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json index da4a164c918..73c00596a7f 100644 --- a/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json +++ b/Telegram/Telegram-iOS/DefaultAppIcon.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram/Telegram-iOS/IconDefault-60@2x.png b/Telegram/Telegram-iOS/IconDefault-60@2x.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-60@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-60@3x.png b/Telegram/Telegram-iOS/IconDefault-60@3x.png deleted file mode 100644 index facbf49ff3a..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-60@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-76.png b/Telegram/Telegram-iOS/IconDefault-76.png deleted file mode 100644 index 07de560340f..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-76@2x.png b/Telegram/Telegram-iOS/IconDefault-76@2x.png deleted file mode 100644 index d71dcd205e7..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-83.5@2x.png b/Telegram/Telegram-iOS/IconDefault-83.5@2x.png deleted file mode 100644 index f51ae17df90..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40.png b/Telegram/Telegram-iOS/IconDefault-Small-40.png deleted file mode 100644 index e2b1ba78909..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png b/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png deleted file mode 100644 index 8d4fe9efe66..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png b/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png deleted file mode 100644 index 9525324b1e6..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small-40@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small.png b/Telegram/Telegram-iOS/IconDefault-Small.png deleted file mode 100644 index 4865bb8b078..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small@2x.png b/Telegram/Telegram-iOS/IconDefault-Small@2x.png deleted file mode 100644 index b9f52c5932e..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/IconDefault-Small@3x.png b/Telegram/Telegram-iOS/IconDefault-Small@3x.png deleted file mode 100644 index 95b278c284f..00000000000 Binary files a/Telegram/Telegram-iOS/IconDefault-Small@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-76.png b/Telegram/Telegram-iOS/New1.alticon/New1-76.png deleted file mode 100644 index c85f9bc45ae..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png deleted file mode 100644 index 32adc011d1a..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png deleted file mode 100644 index 93238e0c7f4..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1@2x.png deleted file mode 100644 index 70ddc32cbec..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1@3x.png b/Telegram/Telegram-iOS/New1.alticon/New1@3x.png deleted file mode 100644 index ced492fd402..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png b/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png deleted file mode 100644 index 6387cb01bde..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_29x29.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png b/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png deleted file mode 100644 index 93c34d10c79..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_58x58.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png b/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png deleted file mode 100644 index fb4f4a61220..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_80x80.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png b/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png deleted file mode 100644 index 1b3e74dfa60..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_87x87.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification.png deleted file mode 100644 index 34afc4fbec0..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png deleted file mode 100644 index e29005ac4e2..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png b/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png deleted file mode 100644 index 54a04f2c27f..00000000000 Binary files a/Telegram/Telegram-iOS/New1.alticon/New1_notification@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-76.png b/Telegram/Telegram-iOS/New2.alticon/New2-76.png deleted file mode 100644 index ca043ed3397..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-76.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png deleted file mode 100644 index d94dc86c61f..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-76@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png deleted file mode 100644 index 813a39a5bdb..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-83.5@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png deleted file mode 100644 index fe2d70eada0..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png deleted file mode 100644 index 6750299df3f..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small-40@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small.png deleted file mode 100644 index 4e0265fa69a..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png deleted file mode 100644 index 37e2e15d18a..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png deleted file mode 100644 index 18156810473..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2-Small@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2@2x.png b/Telegram/Telegram-iOS/New2.alticon/New2@2x.png deleted file mode 100644 index 85de9e3fab6..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2@3x.png deleted file mode 100644 index a083c562d44..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2_notification.png b/Telegram/Telegram-iOS/New2.alticon/New2_notification.png deleted file mode 100644 index d2d9d52e92c..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2_notification.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png b/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png deleted file mode 100644 index caab8e9d8af..00000000000 Binary files a/Telegram/Telegram-iOS/New2.alticon/New2_notification@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png b/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png deleted file mode 100644 index 00ea76d714b..00000000000 Binary files a/Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png b/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png deleted file mode 100644 index 1a67519593f..00000000000 Binary files a/Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png b/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png deleted file mode 100644 index cb953d3546c..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png b/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png deleted file mode 100644 index a3833ef0c77..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png b/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png deleted file mode 100644 index 7eccb509eef..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png b/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png deleted file mode 100644 index a243d72e636..00000000000 Binary files a/Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png b/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png index 7a5a342bc9c..7260909f913 100644 Binary files a/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png and b/Telegram/Telegram-iOS/Resources/intro/telegram_plane1@2x.png differ diff --git a/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png b/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png index 826d68b263e..5bb5b80fc8f 100644 Binary files a/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png and b/Telegram/Telegram-iOS/Resources/intro/telegram_sphere@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png new file mode 100644 index 00000000000..c5c51e0b713 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png new file mode 100644 index 00000000000..c2c05b41922 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBeta.alticon/SGBeta@3x.png differ diff --git a/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png new file mode 100644 index 00000000000..9ece7f08aa2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@2x.png differ diff --git a/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png new file mode 100644 index 00000000000..532041e0c37 Binary files /dev/null and b/Telegram/Telegram-iOS/SGBlack.alticon/SGBlack@3x.png differ diff --git a/Telegram/Telegram-iOS/SGDay.alticon/SGDay@2x.png b/Telegram/Telegram-iOS/SGDay.alticon/SGDay@2x.png new file mode 100644 index 00000000000..2532eb5f8ed 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 00000000000..f368216d0c2 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 00000000000..02c8cbb05c6 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@2x.png differ diff --git a/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png new file mode 100644 index 00000000000..6485f589180 Binary files /dev/null and b/Telegram/Telegram-iOS/SGDefault.alticon/SGDefault@3x.png differ diff --git a/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png b/Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png new file mode 100644 index 00000000000..7a4ee59e65b 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 00000000000..1ec818f6735 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 00000000000..a70a819abdd Binary files /dev/null and b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@2x.png differ diff --git a/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png new file mode 100644 index 00000000000..43a38972b7f Binary files /dev/null and b/Telegram/Telegram-iOS/SGGlass.alticon/SGGlass@3x.png differ diff --git a/Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png b/Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png new file mode 100644 index 00000000000..1e929251b5d 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 00000000000..38c0118975a 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 00000000000..f8131ff5175 Binary files /dev/null and b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@2x.png differ diff --git a/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png new file mode 100644 index 00000000000..e1fc51be8fa Binary files /dev/null and b/Telegram/Telegram-iOS/SGInverted.alticon/SGInverted@3x.png differ diff --git a/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png new file mode 100644 index 00000000000..bc4426140ff Binary files /dev/null and b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@2x.png differ diff --git a/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png new file mode 100644 index 00000000000..f6e25e84cde Binary files /dev/null and b/Telegram/Telegram-iOS/SGLegacy.alticon/SGLegacy@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png new file mode 100644 index 00000000000..1e6dd6862e4 Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png new file mode 100644 index 00000000000..ff12511d04b Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeon.alticon/SGNeon@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png new file mode 100644 index 00000000000..191c60f764f Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png new file mode 100644 index 00000000000..cc37ad00f2f Binary files /dev/null and b/Telegram/Telegram-iOS/SGNeonBlue.alticon/SGNeonBlue@3x.png differ diff --git a/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png new file mode 100644 index 00000000000..df54b8cd97a Binary files /dev/null and b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@2x.png differ diff --git a/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png new file mode 100644 index 00000000000..2c238b101a7 Binary files /dev/null and b/Telegram/Telegram-iOS/SGNight.alticon/SGNight@3x.png differ diff --git a/Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png b/Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png new file mode 100644 index 00000000000..bdeaaac60f1 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 00000000000..30464e4635d 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 00000000000..94d7ec24a09 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@2x.png differ diff --git a/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png new file mode 100644 index 00000000000..d4ac553a2d2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSky.alticon/SGSky@3x.png differ diff --git a/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png new file mode 100644 index 00000000000..5cadd1d556b Binary files /dev/null and b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@2x.png differ diff --git a/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png new file mode 100644 index 00000000000..c5bfeb8f1e2 Binary files /dev/null and b/Telegram/Telegram-iOS/SGSparkling.alticon/SGSparkling@3x.png differ diff --git a/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png new file mode 100644 index 00000000000..e6a99cf622b Binary files /dev/null and b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@2x.png differ diff --git a/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png new file mode 100644 index 00000000000..2e2b2bc6c32 Binary files /dev/null and b/Telegram/Telegram-iOS/SGTitanium.alticon/SGTitanium@3x.png differ diff --git a/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png new file mode 100644 index 00000000000..3f9a26322da Binary files /dev/null and b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@2x.png differ diff --git a/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png new file mode 100644 index 00000000000..b6070b75dec Binary files /dev/null and b/Telegram/Telegram-iOS/SGWhite.alticon/SGWhite@3x.png differ diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png deleted file mode 100644 index 2e52591bc34..00000000000 Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png b/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png deleted file mode 100644 index fb138b52286..00000000000 Binary files a/Telegram/Telegram-iOS/WhiteFilledIcon.alticon/WhiteFilledIcon@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist index 0a71b7adbae..fcdf65836c9 100644 --- a/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - أرسل رسالة لخالد عبر تيليجرام (Telegram) وأخبره أن هديته وصلت إلى المنزل + أرسل رسالة لخالد عبر تيليجرام (Swiftgram) وأخبره أن هديته وصلت إلى المنزل diff --git a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings index 174276a9040..f52a6d14f9a 100644 --- a/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings +++ b/Telegram/Telegram-iOS/ar.lproj/InfoPlist.strings @@ -1,5 +1,5 @@ /* Localized versions of Info.plist keys */ -"CFBundleDisplayName" = "تيليجرام"; + "NSContactsUsageDescription" = "سيقوم تيليجرام برفع جهات الاتصال الخاصة بك باستمرار إلى خوادم التخزين السحابية ذات التشفير العالي لتتمكن من التواصل مع أصدقائك من خلال جميع أجهزتك."; "NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، تيليجرام يحتاج لصلاحيات لعرض الخريطة لهم."; "NSLocationAlwaysAndWhenInUseUsageDescription" = "عندما تختار أن تشارك مكانك بشكل حي مع أصدقائك في المحادثة، يحتاج تيليجرام إلى الوصول لموقعك في الخلفية حتى بعد إغلاق تيليجرام خلال فترة المشاركة."; diff --git a/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ca.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist index 39561210606..25217fc93b4 100644 --- a/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/de.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Sende Lisa eine Telegram-Nachricht, dass ich in 15 Minuten da bin. + Sende Lisa eine Swiftgram-Nachricht, dass ich in 15 Minuten da bin. diff --git a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/en.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings index ca338663408..9f47a12484e 100644 --- a/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings +++ b/Telegram/Telegram-iOS/en.lproj/InfoPlist.strings @@ -1,9 +1,9 @@ /* Localized versions of Info.plist keys */ -"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; -"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; -"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; -"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; +"NSContactsUsageDescription" = "Swiftgram will continuously upload your contacts to Telegram's heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Swiftgram needs access to show them a map."; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing."; +"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Swiftgram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; "NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls."; "NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library."; "NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library."; diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a55e3970c21..8347d4337b0 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"; @@ -2808,7 +2808,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"; @@ -2875,7 +2875,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"; @@ -2907,14 +2907,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"; @@ -3070,12 +3070,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"; @@ -3158,16 +3158,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"; @@ -3311,7 +3311,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"; @@ -3589,9 +3589,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"; @@ -3782,8 +3782,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]"; @@ -3870,7 +3870,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"; @@ -3881,7 +3881,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"; @@ -3912,7 +3912,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"; @@ -3923,7 +3923,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?"; @@ -3996,8 +3996,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."; @@ -4134,7 +4134,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"; @@ -4440,7 +4440,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."; @@ -5063,11 +5063,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"; @@ -5688,7 +5688,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."; @@ -5842,7 +5842,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"; @@ -6186,7 +6186,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."; @@ -6338,7 +6338,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."; @@ -6644,7 +6644,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"; @@ -6945,7 +6945,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"; @@ -7311,8 +7311,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"; @@ -7425,8 +7425,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"; @@ -7470,8 +7470,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"; @@ -7548,7 +7548,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."; @@ -7560,7 +7560,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..."; @@ -7648,7 +7648,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"; @@ -8071,7 +8071,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"; @@ -8631,7 +8631,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"; @@ -9037,7 +9037,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."; @@ -9398,7 +9398,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"; @@ -9567,8 +9567,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."; @@ -9643,7 +9643,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"; @@ -9947,7 +9947,7 @@ Sorry for the inconvenience."; "Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once."; "WebApp.DisclaimerTitle" = "Terms of Use"; -"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."; +"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Swiftgram/Telegram. You must agree to the Terms of Use of mini apps to continue."; "WebApp.DisclaimerAgree" = "I agree to the [Terms of Use]()"; "WebApp.DisclaimerContinue" = "Continue"; "WebApp.Disclaimer_URL" = "https://telegram.org/tos/mini-apps"; diff --git a/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist index fd11102f14d..ae7044bbbcb 100644 --- a/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/es.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Envía un mensaje de Telegram a Alicia diciéndole que estarás allí en 15 minutos + Envía un mensaje de Swiftgram a Alicia diciéndole que estarás allí en 15 minutos diff --git a/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/fr.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/id.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist index 8710a6c624b..550ebf81795 100644 --- a/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/it.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Invia un messaggio su Telegram (Telegramma) ad Alex dicendo che sarò lì tra 10 minuti + Invia un messaggio su Swiftgram (Swiftgramma) ad Alex dicendo che sarò lì tra 10 minuti diff --git a/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist index 932e5f6d28b..980b2b7448b 100644 --- a/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - 앨리스에게 나 15분 안에 도착한다고 Telegram 메시지 보내줘 + 앨리스에게 나 15분 안에 도착한다고 Swiftgram 메시지 보내줘 diff --git a/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ms.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist index 5c709b84ee3..fcc0a842f15 100644 --- a/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Stuur een Telegram-bericht naar Maartje met ik ben er over 15 minuten. + Stuur een Swiftgram-bericht naar Maartje met ik ben er over 15 minuten. diff --git a/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist index 018471dbc6f..2e33f8946e9 100644 --- a/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Enviar uma mensagem no Telegram para a Alice dizendo que eu chegarei lá em 15 minutos + Enviar uma mensagem no Swiftgram para a Alice dizendo que eu chegarei lá em 15 minutos diff --git a/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist index 72fd63d7384..643154cf8c5 100644 --- a/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Отправить Алисе сообщение в Telegram: я буду через 10 минут + Отправить Алисе сообщение в Swiftgram: я буду через 10 минут diff --git a/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/tr.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/uk.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist b/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist index 504ece44836..136ad1e3dba 100644 --- a/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist +++ b/Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist @@ -9,7 +9,7 @@ INSendMessageIntent IntentExamples - Send a Telegram message to Alex saying I'll be there in 10 minutes + Send a Swiftgram message to Alex saying I'll be there in 10 minutes diff --git a/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Telegram/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json index b46b3e5abb8..f1ab5a7e38c 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 a927ff5a6b4..00000000000 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 7559ff4a345..00000000000 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 a5723c6dc40..00000000000 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 ee652ff0a21..00000000000 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 00000000000..a28a393d1ec 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 aa69bfd47b1..00000000000 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 a0b41dc3e5f..00000000000 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 0b6d014c08b..00000000000 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 771bd56e60e..00000000000 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 f81b3a85a68..00000000000 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 5838cb26203..00000000000 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 71fb4bbf9c2..00000000000 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 b2ecb14b80e..0373f2cf076 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 35d1335cea7..a77ec772384 100644 --- a/Telegram/Watch/App/Base.lproj/Interface.storyboard +++ b/Telegram/Watch/App/Base.lproj/Interface.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -264,12 +263,22 @@ - + + + + + + @@ -280,6 +289,7 @@ + @@ -1094,10 +1104,10 @@ contacts found. - + - + - +