Version 11.5.3

This commit is contained in:
Kylmakalle
2024-07-02 19:58:37 +03:00
parent 45fa1b5ddb
commit 7d4b4eaf6c
673 changed files with 34800 additions and 1281 deletions

View File

@@ -1,8 +1,8 @@
name: CI
on:
push:
branches: [ master ]
# push:
# branches: [ master ]
workflow_dispatch:

6
.gitignore vendored
View File

@@ -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/*

5
.gitmodules vendored
View File

@@ -1,7 +1,6 @@
[submodule "submodules/rlottie/rlottie"]
path = submodules/rlottie/rlottie
url=../rlottie.git
url=https://github.com/TelegramMessenger/rlottie.git
[submodule "build-system/bazel-rules/rules_apple"]
path = build-system/bazel-rules/rules_apple
url=https://github.com/ali-fareed/rules_apple.git
@@ -16,7 +15,7 @@ url=https://github.com/bazelbuild/rules_swift.git
url = https://github.com/telegramdesktop/libtgvoip.git
[submodule "submodules/TgVoipWebrtc/tgcalls"]
path = submodules/TgVoipWebrtc/tgcalls
url=../tgcalls.git
url=https://github.com/TelegramMessenger/tgcalls.git
[submodule "third-party/libvpx/libvpx"]
path = third-party/libvpx/libvpx
url = https://github.com/webmproject/libvpx.git

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"sweetpad.build.xcodeWorkspacePath": "Telegram/Swiftgram.xcodeproj/project.xcworkspace"
}

View File

@@ -1,3 +1,16 @@
# Swiftgram
Supercharged Telegram fork for iOS
[<img src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" height="50">](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.

View File

@@ -0,0 +1,9 @@
filegroup(
name = "AppleStyleFolders",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
filegroup(
name = "ChatControllerImplExtension",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -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<Bool, NoError>] = messageIds.compactMap({ id -> Signal<Bool, NoError>? in
guard let id = id else {
return nil
}
return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id)
|> mapToSignal { status, _ -> Signal<Bool, NoError> 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))
})
} ))
}
})
})
}
}

0
Swiftgram/FLEX/BUILD Normal file
View File

68
Swiftgram/FLEX/FLEX.BUILD Normal file
View File

@@ -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"],
)

View File

@@ -0,0 +1,3 @@
--maxwidth 100
--indent 4
--disable redundantSelf

View File

@@ -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"],
),
)

View File

@@ -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
```

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
import UIKit
@objc(Application) class Application: UIApplication {
}

View File

@@ -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)
}
}

View File

@@ -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<PlaygroundThemeResourceCacheHolder>(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
)
)

View File

@@ -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<MySwiftUIView>(
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
}

View File

@@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, @"Application", @"AppDelegate");
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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",
],
)

View File

@@ -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?()
}
}

25
Swiftgram/SGAPI/BUILD Normal file
View File

@@ -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",
],
)

View File

@@ -0,0 +1,145 @@
import Foundation
import SwiftSignalKit
import SGConfig
import SGLogging
import SGSimpleSettings
import SGWebAppExtensions
import SGWebSettingsScheme
import SGRequests
import SGRegDateScheme
private let API_VERSION: String = "0"
private func buildApiUrl(_ endpoint: String) -> String {
return "\(SG_CONFIG.apiUrl)/v\(API_VERSION)/\(endpoint)"
}
public let SG_API_AUTHORIZATION_HEADER = "Authorization"
public let SG_API_DEVICE_TOKEN_HEADER = "Device-Token"
private enum HTTPRequestError {
case network
}
public enum SGAPIError {
case generic(String? = nil)
}
public func getSGSettings(token: String) -> Signal<SGWebSettings, SGAPIError> {
return Signal { subscriber in
let url = URL(string: buildApiUrl("settings"))!
let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"]
let completed = Atomic<Bool>(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<Void, SGAPIError> {
return Signal { subscriber in
let url = URL(string: buildApiUrl("settings"))!
let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"]
let completed = Atomic<Bool>(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<RegDate, SGAPIError> {
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<Bool>(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()
}
}
}
}

View File

@@ -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",
],
)

View File

@@ -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<String, SGAPITokenError> {
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<String, SGAPITokenError> 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<String, SGAPITokenError> {
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<EnginePeer?, NoError> 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()
}
}
}

View File

@@ -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",
],
)

View File

@@ -0,0 +1,49 @@
import Foundation
import SGAPIToken
import SGAPI
import SGLogging
import AccountContext
import SGSimpleSettings
import TelegramCore
public func updateSGWebSettingsInteractivelly(context: AccountContext) {
let _ = getSGApiToken(context: context).startStandalone(next: { token in
let _ = getSGSettings(token: token).startStandalone(next: { webSettings in
SGLogger.shared.log("SGAPI", "New SGWebSettings for id \(context.account.peerId.id._internalGetInt64Value()): \(webSettings) ")
SGSimpleSettings.shared.canUseStealthMode = webSettings.global.storiesAvailable
let _ = (context.account.postbox.transaction { transaction in
updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in
var configuration = configuration
configuration.sgWebSettings = webSettings
return configuration
})
}).startStandalone()
}, error: { e in
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
SGLogger.shared.log("SGAPI", errorMessage)
}
})
}, error: { e in
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
SGLogger.shared.log("SGAPI", errorMessage)
}
})
}
public func postSGWebSettingsInteractivelly(context: AccountContext, data: [String: Any]) {
let _ = getSGApiToken(context: context).startStandalone(next: { token in
let _ = postSGSettings(token: token, data: data).startStandalone(error: { e in
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
SGLogger.shared.log("SGAPI", errorMessage)
}
})
}, error: { e in
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
SGLogger.shared.log("SGAPI", errorMessage)
}
})
}

View File

@@ -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",
],
)

View File

@@ -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
}

18
Swiftgram/SGConfig/BUILD Normal file
View File

@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGConfig",
module_name = "SGConfig",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/BuildConfig:BuildConfig"
],
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,20 @@
import Foundation
import BuildConfig
public struct SGConfig: Codable {
public var apiUrl: String = "https://api.swiftgram.app"
public var webappUrl: String = "https://my.swiftgram.app"
public var botUsername: String = "SwiftgramBot"
}
private func parseSGConfig(_ jsonString: String) -> SGConfig {
let jsonData = Data(jsonString.utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return (try? decoder.decode(SGConfig.self, from: jsonData)) ?? SGConfig()
}
private let baseAppBundleId = Bundle.main.bundleIdentifier!
private let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
public let SG_CONFIG: SGConfig = parseSGConfig(buildConfig.sgConfig)
public let SG_API_WEBAPP_URL_PARSED = URL(string: SG_CONFIG.webappUrl)!

View File

@@ -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",
],
)

View File

@@ -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<Bool, Error> {
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)
}
}

View File

@@ -0,0 +1,9 @@
filegroup(
name = "SGDBReset",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,47 @@
import UIKit
import Foundation
import SGLogging
private let dbResetKey = "sg_db_reset"
public func sgDBResetIfNeeded(databasePath: String, present: ((UIViewController) -> ())?) {
guard UserDefaults.standard.bool(forKey: dbResetKey) else {
return
}
NSLog("[SG.DBReset] Resetting DB with system settings")
let alert = UIAlertController(
title: "Database reset.\nPlease wait...",
message: nil,
preferredStyle: .alert
)
present?(alert)
do {
let _ = try FileManager.default.removeItem(atPath: databasePath)
NSLog("[SG.DBReset] Done. Reset completed")
let successAlert = UIAlertController(
title: "Database reset completed",
message: nil,
preferredStyle: .alert
)
successAlert.addAction(UIAlertAction(title: "Restart App", style: .cancel) { _ in
exit(0)
})
successAlert.addAction(UIAlertAction(title: "OK", style: .default))
alert.dismiss(animated: false) {
present?(successAlert)
}
} catch {
NSLog("[SG.DBReset] ERROR. Failed to reset database: \(error)")
let failAlert = UIAlertController(
title: "ERROR. Failed to reset database",
message: "\(error)",
preferredStyle: .alert
)
alert.dismiss(animated: false) {
present?(failAlert)
}
}
UserDefaults.standard.set(false, forKey: dbResetKey)
// let semaphore = DispatchSemaphore(value: 0)
// semaphore.wait()
}

48
Swiftgram/SGDebugUI/BUILD Normal file
View File

@@ -0,0 +1,48 @@
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",
"//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",
],
)

View File

@@ -0,0 +1,986 @@
import Foundation
import UniformTypeIdentifiers
import SGItemListUI
import UndoUI
import AccountContext
import Display
import TelegramCore
import Postbox
import ItemListUI
import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
// Optional
import SGSimpleSettings
import SGLogging
import OverlayStatusController
#if DEBUG
import FLEX
#endif
import Security
let BACKUP_SERVICE: String = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
enum KeychainError: Error {
case duplicateEntry
case unknown(OSStatus)
case itemNotFound
case invalidItemFormat
}
class KeychainBackupManager {
static let shared = KeychainBackupManager()
private let service = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
private init() {}
// MARK: - Save Credentials
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
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
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
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
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)
}
}
}
struct SessionBackup: Codable {
var name: String? = nil
var date: Date = Date()
let accountRecord: AccountRecord<TelegramAccountManagerTypes.Attribute>
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 {
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("Last Backup: \(formattedDate)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(isLoggedIn ? "Logged In" : "Logged Out")
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(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 {
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 {
let (view, accountsWithInfo) = await combineLatest(signal, signal2).awaitable()
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("OK: \(restoredSessions) Sessions restored", 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 {
print("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)
print("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: "Delete All Backups?", text: "All sessions will be removed from Keychain.\n\nAccounts will not be logged out from Swiftgram.", 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 {
print("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: "Delete 1 Backup?", text: "\(session.name ?? "\(session.userId)") session will be removed from Keychain.\n\nAccount will not be logged out from Swiftgram.", 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 {
print("Error deleting session: \(e)")
}
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
])
wrapperController?.present(controller, in: .window(.root), with: nil)
}
#if DEBUG
private func performRemoveSessionFromApp(session: SessionBackup) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = textAlertController(context: context, title: "Remove session from App?", text: "\(session.name ?? "\(session.userId)") session will be removed from app? Account WILL BE logged out of Swiftgram.", 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)
}
#endif
var body: some View {
List {
Section(header: Text("Actions")) {
Button(action: performBackup) {
HStack {
Image(systemName: "key.fill")
.frame(width: 30)
Text("Backup to Keychain")
Spacer()
}
}
Button(action: performRestore) {
HStack {
Image(systemName: "arrow.2.circlepath")
.frame(width: 30)
Text("Restore from Keychain")
Spacer()
}
}
Button(action: performDeleteAll) {
HStack {
Image(systemName: "trash")
.frame(width: 30)
Text("Delete Keychain Backup")
}
}
.foregroundColor(.red)
// Text("Removing sessions from Keychain. This will not affect logged-in accounts.")
// .font(.caption)
}
Section(header: Text("Backups")) {
ForEach(sessions, id: \.peerIdInternal) { session in
SessionBackupRow(
backup: session,
isLoggedIn: loggedInPeerIDs.contains(session.peerIdInternal)
)
.contextMenu {
Button(action: {
performDelete(session)
}, label: {
HStack(spacing: 4) {
Text("Delete from Backup")
Image(systemName: "trash")
}
})
#if DEBUG
Button(action: {
performRemoveSessionFromApp(session: session)
}, label: {
HStack(spacing: 4) {
Text("Remove from App")
Image(systemName: "trash")
}
})
#endif
}
}
// .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)
}
}
print("Will check logged in accounts")
if loggedInPeerIDs != result {
print("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 {
print("IMPORT ERROR: \(e)")
}
}
} catch let e {
print("Error getting all sessions: \(e)")
}
return sessions
}
func backupSessionsFromView(_ view: AccountRecordsView<TelegramAccountManagerTypes>, accountsWithInfo: [AccountWithInfo] = []) {
var recordsToBackup: [Int64: AccountRecord<TelegramAccountManagerTypes.Attribute>] = [:]
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 {
print("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 = "Session Backup" //i18n("BackupManager.Title", strings.baseLanguageCode)
let swiftUIView = SGSwiftUIView<SessionBackupManagerView>(
navigationBarHeight: legacyController.navigationBarHeightModel,
containerViewLayout: legacyController.containerViewLayoutModel,
content: {
SessionBackupManagerView(wrapperController: legacyController, context: context)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}
@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 {
@Binding var newKeyword: String
let onAdd: () -> Void
var body: some View {
HStack {
TextField("Enter keyword", 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?
@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("Message Filter")
.font(.title)
.bold()
Text("Remove distraction and reduce visibility of messages containing keywords below.\nKeywords are case-sensitive.")
.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("Keywords")) {
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 = MessageFilterView(wrapperController: legacyController)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}
private enum SGDebugControllerSection: Int32, SGItemListSection {
case base
}
private enum SGDebugDisclosureLink: String {
case sessionBackupManager
case messageFilter
}
private enum SGDebugActions: String {
case flexing
case fileManager
case clearRegDateCache
}
private enum SGDebugToggles: String {
case forceImmediateShareSheet
case legacyNotificationsFix
}
private typealias SGDebugControllerEntry = SGItemListUIEntry<SGDebugControllerSection, SGDebugToggles, AnyHashable, AnyHashable, SGDebugDisclosureLink, SGDebugActions>
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
if SGSimpleSettings.shared.b {
entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "Session Backup"))
entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "Message Filter"))
}
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
entries.append(.toggle(id: id.count, section: .base, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[Legacy] 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<SGDebugToggles, AnyHashable, AnyHashable, SGDebugDisclosureLink, SGDebugActions>(context: context, setBoolValue: { toggleName, value in
switch toggleName {
case .forceImmediateShareSheet:
SGSimpleSettings.shared.forceSystemSharing = value
case .legacyNotificationsFix:
SGSimpleSettings.shared.legacyNotificationsFix = value
}
}, openDisclosureLink: { link in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch (link) {
case .sessionBackupManager:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
} else {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
), nil)
}
case .messageFilter:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
} else {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
), nil)
}
}
}, action: { actionType in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch actionType {
case .clearRegDateCache:
SGLogger.shared.log("SGDebug", "Regdate cache cleanup init")
/*
let spinner = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(spinner, nil)
*/
SGSimpleSettings.shared.regDateCache.drop()
SGLogger.shared.log("SGDebug", "Regdate cache cleanup succesfull")
presentControllerImpl?(okUndoController("OK: Regdate cache cleaned", presentationData), nil)
/*
Queue.mainQueue().async() { [weak spinner] in
spinner?.dismiss()
}
*/
case .flexing:
#if DEBUG
FLEXManager.shared.toggleExplorer()
#endif
case .fileManager:
#if DEBUG
let baseAppBundleId = Bundle.main.bundleIdentifier!
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
if let maybeAppGroupUrl = maybeAppGroupUrl {
if let fileManager = FLEXFileBrowserController(path: maybeAppGroupUrl.path) {
FLEXManager.shared.showExplorer()
let flexNavigation = FLEXNavigationController(rootViewController: fileManager)
FLEXManager.shared.presentTool({ return flexNavigation })
}
} else {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Empty path", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
}
#endif
}
})
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
}

View File

@@ -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",
],
)

View File

@@ -0,0 +1,31 @@
import SwiftSignalKit
import DeviceCheck
public enum SGDeviceTokenError {
case unsupportedDevice
case generic(String)
}
public func getDeviceToken() -> Signal<String, SGDeviceTokenError> {
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 {
}
}
}

View File

@@ -0,0 +1,9 @@
filegroup(
name = "SGDoubleTapMessageAction",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -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
}
}

View File

@@ -0,0 +1,9 @@
filegroup(
name = "SGEmojiKeyboardDefaultFirst",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -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
}

9
Swiftgram/SGIQTP/BUILD Normal file
View File

@@ -0,0 +1,9 @@
filegroup(
name = "SGIQTP",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,76 @@
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<SGIQTPResponse?, NoError> {
#if DEBUG
SGLogger.shared.log("SGIQTP", "Query: \(query)")
#else
SGLogger.shared.log("SGIQTP", "Query")
#endif
return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
SGLogger.shared.log("SGIQTP", "Failed to resolve peer")
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
SGLogger.shared.log("SGIQTP", "Empty peer")
return .single(nil)
}
return engine.messages.requestChatContextResults(botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
SGLogger.shared.log("SGIQTP", "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", "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", "Response: \(response)")
return response
}
}

View File

@@ -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",
],
)

View File

@@ -0,0 +1,333 @@
// MARK: Swiftgram
import SGLogging
import SGSimpleSettings
import SGStrings
import SGAPIToken
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKit
import MessageUI
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AppBundle
import WebKit
import PeerNameColorScreen
public class SGItemListCounter {
private var _count = 0
public init() {}
public var count: Int {
_count += 1
return _count
}
public func increment(_ amount: Int) {
_count += amount
}
public func countWith(_ amount: Int) -> Int {
_count += amount
return count
}
}
public protocol SGItemListSection: Equatable {
var rawValue: Int32 { get }
}
public final class SGItemListArguments<BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable> {
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<Section: SGItemListSection, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>: 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<BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>
switch self {
case let .header(_, _, string, badge):
return ItemListSectionHeaderItem(presentationData: presentationData, text: string, badge: badge, sectionId: self.section)
case let .toggle(_, _, setting, value, text, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.setBoolValue(setting, value)
})
case let .notice(_, _, string):
return ItemListTextItem(presentationData: presentationData, text: .markdown(string), sectionId: self.section)
case let .disclosure(_, _, link, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks) {
arguments.openDisclosureLink(link)
}
case let .percentageSlider(_, _, setting, value):
return SliderPercentageItem(
theme: presentationData.theme,
strings: presentationData.strings,
value: value,
sectionId: self.section,
updated: { value in
arguments.updateSliderValue(setting, value)
}
)
case let .peerColorDisclosurePreview(_, _, name, color):
return ItemListDisclosureItem(presentationData: presentationData, title: " ", enabled: false, label: name, labelStyle: .semitransparentBadge(color), centerLabelAlignment: true, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
})
case let .oneFromManySelector(_, _, settingName, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setOneFromManyValue(settingName)
})
case let .action(_, _, actionType, text, kind):
return ItemListActionItem(presentationData: presentationData, title: text, kind: kind, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.action(actionType)
})
case let .searchInput(_, _, title, text, placeholder):
return ItemListSingleLineInputItem(presentationData: presentationData, title: title, text: text, placeholder: placeholder, returnKeyType: .done, spacing: 3.0, clearType: .always, selectAllOnFocus: true, secondaryStyle: true, sectionId: self.section, textUpdated: { input in arguments.searchInput(input) }, action: {}, dismissKeyboardOnEnter: true)
}
}
}
public func filterSGItemListUIEntrires<Section: SGItemListSection & Hashable, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>(
entries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>],
by searchQuery: String?
) -> [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] {
guard let query = searchQuery?.lowercased(), !query.isEmpty else {
return entries
}
var sectionIdsForEntireIncludion: Set<ItemListSectionId> = []
var sectionIdsWithMatches: Set<ItemListSectionId> = []
var filteredEntries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] = []
func entryMatches(_ entry: SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>, 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
}

View File

@@ -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<Void, NoError>?, (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))
}
}

19
Swiftgram/SGLogging/BUILD Normal file
View File

@@ -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",
],
)

View File

@@ -0,0 +1,236 @@
import Foundation
import SwiftSignalKit
import ManagedFile
private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility)
private var sharedLogger: SGLogger?
private let binaryEventMarker: UInt64 = 0xcadebabef00dcafe
private func rootPathForBasePath(_ appGroupPath: String) -> String {
return appGroupPath + "/telegram-data"
}
public final class SGLogger {
private let queue = Queue(name: "app.swiftgram.ios.log", qos: .utility)
private let maxLength: Int = 2 * 1024 * 1024
private let maxShortLength: Int = 1 * 1024 * 1024
private let maxFiles: Int = 20
private let rootPath: String
private let basePath: String
private var file: (ManagedFile, Int)?
private var shortFile: (ManagedFile, Int)?
public static let sgLogsPath = "/logs/app-logs-sg"
public var logToFile: Bool = true
public var logToConsole: Bool = true
public var redactSensitiveData: Bool = true
public static func setSharedLogger(_ logger: SGLogger) {
sharedLogger = logger
}
public static var shared: SGLogger {
if let sharedLogger = sharedLogger {
return sharedLogger
} else {
print("SGLogger setup...")
guard let baseAppBundleId = Bundle.main.bundleIdentifier else {
print("Can't setup logger (1)!")
return SGLogger(rootPath: "", basePath: "")
}
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
print("Can't setup logger (2)!")
return SGLogger(rootPath: "", basePath: "")
}
let newRootPath = rootPathForBasePath(appGroupUrl.path)
let newLogsPath = newRootPath + sgLogsPath
let _ = try? FileManager.default.createDirectory(atPath: newLogsPath, withIntermediateDirectories: true, attributes: nil)
self.setSharedLogger(SGLogger(rootPath: newRootPath, basePath: newLogsPath))
if let sharedLogger = sharedLogger {
return sharedLogger
} else {
print("Can't setup logger (3)!")
return SGLogger(rootPath: "", basePath: "")
}
}
}
public init(rootPath: String, basePath: String) {
self.rootPath = rootPath
self.basePath = basePath
}
public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> {
return Signal { subscriber in
self.queue.async {
let logsPath: String
if let prefix = prefix {
logsPath = self.rootPath + prefix
} else {
logsPath = self.basePath
}
var result: [(Date, String, String)] = []
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
for url in files {
if url.lastPathComponent.hasPrefix("log-") {
if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate {
result.append((creationDate, url.lastPathComponent, url.path))
}
}
}
}
result.sort(by: { $0.0 < $1.0 })
subscriber.putNext(result.map { ($0.1, $0.2) })
subscriber.putCompletion()
}
return EmptyDisposable
}
}
public func collectLogs(basePath: String) -> Signal<[(String, String)], NoError> {
return Signal { subscriber in
self.queue.async {
let logsPath: String = basePath
var result: [(Date, String, String)] = []
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
for url in files {
if url.lastPathComponent.hasPrefix("log-") {
if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate {
result.append((creationDate, url.lastPathComponent, url.path))
}
}
}
}
result.sort(by: { $0.0 < $1.0 })
subscriber.putNext(result.map { ($0.1, $0.2) })
subscriber.putCompletion()
}
return EmptyDisposable
}
}
public func log(_ tag: String, _ what: @autoclosure () -> String) {
if !self.logToFile && !self.logToConsole {
return
}
let string = what()
var rawTime = time_t()
time(&rawTime)
var timeinfo = tm()
localtime_r(&rawTime, &timeinfo)
var curTime = timeval()
gettimeofday(&curTime, nil)
let milliseconds = curTime.tv_usec / 1000
var consoleContent: String?
if self.logToConsole {
let content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string])
consoleContent = content
print(content)
}
if self.logToFile {
self.queue.async {
let content: String
if let consoleContent = consoleContent {
content = consoleContent
} else {
content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string])
}
var currentFile: ManagedFile?
var openNew = false
if let (file, length) = self.file {
if length >= self.maxLength {
self.file = nil
openNew = true
} else {
currentFile = file
}
} else {
openNew = true
}
if openNew {
let _ = try? FileManager.default.createDirectory(atPath: self.basePath, withIntermediateDirectories: true, attributes: nil)
var createNew = false
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
var minCreationDate: (Date, URL)?
var maxCreationDate: (Date, URL)?
var count = 0
for url in files {
if url.lastPathComponent.hasPrefix("log-") {
if let values = try? url.resourceValues(forKeys: Set([URLResourceKey.creationDateKey])), let creationDate = values.creationDate {
count += 1
if minCreationDate == nil || minCreationDate!.0 > creationDate {
minCreationDate = (creationDate, url)
}
if maxCreationDate == nil || maxCreationDate!.0 < creationDate {
maxCreationDate = (creationDate, url)
}
}
}
}
if let (_, url) = minCreationDate, count >= self.maxFiles {
let _ = try? FileManager.default.removeItem(at: url)
}
if let (_, url) = maxCreationDate {
var value = stat()
if stat(url.path, &value) == 0 && Int(value.st_size) < self.maxLength {
if let file = ManagedFile(queue: self.queue, path: url.path, mode: .append) {
self.file = (file, Int(value.st_size))
currentFile = file
}
} else {
createNew = true
}
} else {
createNew = true
}
}
if createNew {
let fileName = String(format: "log-%d-%d-%d_%02d-%02d-%02d.%03d.txt", arguments: [Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds)])
let path = self.basePath + "/" + fileName
if let file = ManagedFile(queue: self.queue, path: path, mode: .append) {
self.file = (file, 0)
currentFile = file
}
}
}
if let currentFile = currentFile {
if let data = content.data(using: .utf8) {
data.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let _ = currentFile.write(bytes, count: data.count)
}
var newline: UInt8 = 0x0a
let _ = currentFile.write(&newline, count: 1)
if let file = self.file {
self.file = (file.0, file.1 + data.count + 1)
} else {
assertionFailure()
}
}
}
}
}
}
}

View File

@@ -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))
//}

27
Swiftgram/SGRegDate/BUILD Normal file
View File

@@ -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",
],
)

View File

@@ -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<RegDate?, NoError> {
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()
}
}
}

View File

@@ -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",
],
)

View File

@@ -0,0 +1,7 @@
import Foundation
public struct RegDate: Codable {
public let from: Int64
public let to: Int64
public let validUntil: Int64
}

View File

@@ -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",
],
)

View File

@@ -0,0 +1,72 @@
import Foundation
import SwiftSignalKit
public func requestsDownload(url: URL) -> Signal<(Data, URLResponse?), Error?> {
return Signal { subscriber in
let completed = Atomic<Bool>(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<Bool>(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<Bool>(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()
}
}
}
}

View File

@@ -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"]
)

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key>
<string>Reset.Notice</string>
<key>Title</key>
<string>Reset.Title</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Reset.Toggle</string>
<key>Key</key>
<string>sg_db_reset</string>
<key>DefaultValue</key>
<false/>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,5 @@
/* A single strings file, whose title is specified in your preferences schema. The strings files provide the localized content to display to the user for each of your preferences. */
"Reset.Title" = "TROUBLESHOOTING";
"Reset.Toggle" = "Reset caches on next launch";
"Reset.Notice" = "Use in case you're stuck and can't open the app. This WILL NOT logout your accounts, but all secret chats will be lost.";

View File

@@ -0,0 +1,3 @@
"Reset.Title" = "РЕШЕНИЕ ПРОБЛЕМ";
"Reset.Toggle" = "Сбросить кэш при следующем запуске";
"Reset.Notice" = "Используйте, если приложение вылетает или не загружается. Эта опция НЕ СБРАСЫВАЕТ ваши аккаунты, но удалит все секретные чаты.";

View File

@@ -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",
],
)

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_lt_savetocloud.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "swiftgram_context_menu.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,81 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.000000 2.964844 cm
0.000000 0.000000 0.000000 scn
15.076375 10.766671 m
15.473662 11.399487 14.937258 12.204764 14.200223 12.081993 c
9.059459 11.225675 l
8.855769 11.191745 8.670359 11.348825 8.670359 11.555322 c
8.670359 18.524288 l
8.670359 19.289572 7.652856 19.554642 7.279467 18.886631 c
1.036950 7.718488 l
0.658048 7.040615 1.293577 6.244993 2.038416 6.464749 c
9.378864 8.630468 l
9.637225 8.706696 9.814250 8.373775 9.606588 8.202201 c
6.918006 5.980853 l
6.462659 5.604639 6.199009 5.044809 6.199009 4.454151 c
6.199009 -0.793964 l
6.199009 -1.539309 7.174314 -1.820084 7.570620 -1.188831 c
15.076375 10.766671 l
h
f*
n
Q
endstream
endobj
3 0 obj
702
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000792 00000 n
0000000814 00000 n
0000000987 00000 n
0000001061 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1120
%%EOF

View File

@@ -0,0 +1,13 @@
{
"images" : [
{
"filename" : "Swiftgram.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,242 @@
%PDF-1.7
1 0 obj
<< /Length 2 0 R
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
/Domain [ 0.000000 1.000000 ]
/FunctionType 4
>>
stream
{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop }
endstream
endobj
2 0 obj
339
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ]
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
/ColorSpace /DeviceRGB
/Function 1 0 R
/Domain [ 0.000000 1.000000 ]
/ShadingType 2
/Extend [ true true ]
>>
/PatternType 2
/Type /Pattern
>> >> >>
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
/Pattern cs
/P1 scn
0.000000 320.853333 m
0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c
24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c
90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c
320.853333 512.000000 l
387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c
469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c
512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c
512.000000 191.146667 l
512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c
487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c
421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c
191.146667 0.000000 l
124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c
42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c
0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c
0.000000 320.853333 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 119.500000 103.400391 cm
1.000000 1.000000 1.000000 scn
256.015533 182.826599 m
262.761963 193.572601 253.653152 207.247192 241.137390 205.162399 c
153.840836 190.621048 l
150.381927 190.044891 147.233429 192.712296 147.233429 196.218872 c
147.233429 314.560455 l
147.233429 327.555908 129.954987 332.057098 123.614365 320.713440 c
17.608702 131.064743 l
11.174477 119.553635 21.966566 106.042999 34.614845 109.774734 c
159.264740 146.551285 l
163.652023 147.845703 166.658112 142.192291 163.131760 139.278763 c
117.476318 101.557587 l
109.743965 95.169006 105.266861 85.662384 105.266861 75.632263 c
105.266861 -13.487152 l
105.266861 -26.143982 121.828712 -30.911926 128.558456 -20.192505 c
256.015533 182.826599 l
h
f*
n
Q
endstream
endobj
4 0 obj
1771
endobj
5 0 obj
<< /Length 6 0 R
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
/Domain [ 0.000000 1.000000 ]
/FunctionType 4
>>
stream
{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop }
endstream
endobj
6 0 obj
339
endobj
7 0 obj
<< /Type /XObject
/Length 8 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ]
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
/ColorSpace /DeviceRGB
/Function 5 0 R
/Domain [ 0.000000 1.000000 ]
/ShadingType 2
/Extend [ true true ]
>>
/PatternType 2
/Type /Pattern
>> >> >>
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
/Pattern cs
/P1 scn
0.000000 320.853333 m
0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c
24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c
90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c
320.853333 512.000000 l
387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c
469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c
512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c
512.000000 191.146667 l
512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c
487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c
421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c
191.146667 0.000000 l
124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c
42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c
0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c
0.000000 320.853333 l
h
f
n
Q
endstream
endobj
8 0 obj
1006
endobj
9 0 obj
<< /XObject << /X1 3 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 7 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
10 0 obj
<< /Length 11 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
11 0 obj
46
endobj
12 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 512.000000 512.000000 ]
/Resources 9 0 R
/Contents 10 0 R
/Parent 13 0 R
>>
endobj
13 0 obj
<< /Kids [ 12 0 R ]
/Count 1
/Type /Pages
>>
endobj
14 0 obj
<< /Pages 13 0 R
/Type /Catalog
>>
endobj
xref
0 15
0000000000 65535 f
0000000010 00000 n
0000000533 00000 n
0000000555 00000 n
0000003331 00000 n
0000003354 00000 n
0000003877 00000 n
0000003899 00000 n
0000005910 00000 n
0000005933 00000 n
0000006231 00000 n
0000006335 00000 n
0000006357 00000 n
0000006535 00000 n
0000006611 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 14 0 R
/Size 15
>>
startxref
6672
%%EOF

View File

@@ -0,0 +1,672 @@
// MARK: Swiftgram
import SGLogging
import SGSimpleSettings
import SGStrings
import SGAPIToken
import SGItemListUI
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKit
import MessageUI
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AppBundle
import WebKit
import PeerNameColorScreen
import UndoUI
private enum SGControllerSection: Int32, SGItemListSection {
case search
case content
case tabs
case folders
case chatList
case profiles
case stories
case translation
case photo
case stickers
case videoNotes
case contextMenu
case accountColors
case other
}
private enum SGBoolSetting: String {
case hidePhoneInSettings
case showTabNames
case showContactsTab
case showCallsTab
case foldersAtBottom
case startTelescopeWithRearCam
case hideStories
case uploadSpeedBoost
case showProfileId
case warnOnStoriesOpen
case sendWithReturnKey
case rememberLastFolder
case sendLargePhotos
case storyStealthMode
case disableSwipeToRecordStory
case disableDeleteChatSwipeOption
case quickTranslateButton
case hideReactions
case showRepostToStory
case contextShowSelectFromUser
case contextShowSaveToCloud
case contextShowHideForwardName
case contextShowRestrict
case contextShowReport
case contextShowReply
case contextShowPin
case contextShowSaveMedia
case contextShowMessageReplies
case contextShowJson
case disableScrollToNextChannel
case disableScrollToNextTopic
case disableChatSwipeOptions
case disableGalleryCamera
case disableSendAsButton
case disableSnapDeletionEffect
case stickerTimestamp
case hideRecordingButton
case hideTabBar
case showDC
case showCreationDate
case showRegDate
case compactChatList
case compactFolderNames
case allChatsHidden
case defaultEmojisFirst
case messageDoubleTapActionOutgoingEdit
case wideChannelPosts
case forceEmojiTab
case forceBuiltInMic
case hideChannelBottomButton
case confirmCalls
case swipeForVideoPIP
}
private enum SGOneFromManySetting: String {
case bottomTabStyle
case downloadSpeedBoost
case allChatsTitleLengthOverride
// case allChatsFolderPositionOverride
}
private enum SGSliderSetting: String {
case accountColorsSaturation
case outgoingPhotoQuality
case stickerSize
}
private enum SGDisclosureLink: String {
case contentSettings
case languageSettings
}
private struct PeerNameColorScreenState: Equatable {
var updatedNameColor: PeerNameColor?
var updatedBackgroundEmojiId: Int64?
}
private struct SGSettingsControllerState: Equatable {
var searchQuery: String?
}
private typealias SGControllerEntry = SGItemListUIEntry<SGControllerSection, SGBoolSetting, SGSliderSetting, SGOneFromManySetting, SGDisclosureLink, AnyHashable>
private func SGControllerEntries(presentationData: PresentationData, callListSettings: CallListSettings, experimentalUISettings: ExperimentalUISettings, SGSettings: SGUISettings, appConfiguration: AppConfiguration, nameColors: PeerNameColors, state: SGSettingsControllerState) -> [SGControllerEntry] {
let lang = presentationData.strings.baseLanguageCode
var entries: [SGControllerEntry] = []
let id = SGItemListCounter()
entries.append(.searchInput(id: id.count, section: .search, title: NSAttributedString(string: "🔍"), text: state.searchQuery ?? "", placeholder: presentationData.strings.Common_Search))
if appConfiguration.sgWebSettings.global.canEditSettings {
entries.append(.disclosure(id: id.count, section: .content, link: .contentSettings, text: i18n("Settings.ContentSettings", lang)))
} else {
id.increment(1)
}
entries.append(.header(id: id.count, section: .tabs, text: i18n("Settings.Tabs.Header", lang), badge: nil))
entries.append(.toggle(id: id.count, section: .tabs, settingName: .hideTabBar, value: SGSimpleSettings.shared.hideTabBar, text: i18n("Settings.Tabs.HideTabBar", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showContactsTab, value: callListSettings.showContactsTab, text: i18n("Settings.Tabs.ShowContacts", lang), enabled: !SGSimpleSettings.shared.hideTabBar))
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showCallsTab, value: callListSettings.showTab, text: presentationData.strings.CallSettings_TabIcon, enabled: !SGSimpleSettings.shared.hideTabBar))
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showTabNames, value: SGSimpleSettings.shared.showTabNames, text: i18n("Settings.Tabs.ShowNames", lang), enabled: !SGSimpleSettings.shared.hideTabBar))
entries.append(.header(id: id.count, section: .folders, text: presentationData.strings.Settings_ChatFolders.uppercased(), badge: nil))
entries.append(.toggle(id: id.count, section: .folders, settingName: .foldersAtBottom, value: experimentalUISettings.foldersTabAtBottom, text: i18n("Settings.Folders.BottomTab", lang), enabled: true))
entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .bottomTabStyle, text: i18n("Settings.Folders.BottomTabStyle", lang), value: i18n("Settings.Folders.BottomTabStyle.\(SGSimpleSettings.shared.bottomTabStyle)", lang), enabled: experimentalUISettings.foldersTabAtBottom))
entries.append(.toggle(id: id.count, section: .folders, settingName: .allChatsHidden, value: SGSimpleSettings.shared.allChatsHidden, text: i18n("Settings.Folders.AllChatsHidden", lang, presentationData.strings.ChatList_Tabs_AllChats), enabled: true))
#if DEBUG
// entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsFolderPositionOverride, text: i18n("Settings.Folders.AllChatsPlacement", lang), value: i18n("Settings.Folders.AllChatsPlacement.\(SGSimpleSettings.shared.allChatsFolderPositionOverride)", lang), enabled: true))
#endif
entries.append(.toggle(id: id.count, section: .folders, settingName: .compactFolderNames, value: SGSimpleSettings.shared.compactFolderNames, text: i18n("Settings.Folders.CompactNames", lang), enabled: SGSimpleSettings.shared.bottomTabStyle != SGSimpleSettings.BottomTabStyleValues.ios.rawValue))
entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsTitleLengthOverride, text: i18n("Settings.Folders.AllChatsTitle", lang), value: i18n("Settings.Folders.AllChatsTitle.\(SGSimpleSettings.shared.allChatsTitleLengthOverride)", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .folders, settingName: .rememberLastFolder, value: SGSimpleSettings.shared.rememberLastFolder, text: i18n("Settings.Folders.RememberLast", lang), enabled: true))
entries.append(.notice(id: id.count, section: .folders, text: i18n("Settings.Folders.RememberLast.Notice", lang)))
entries.append(.header(id: id.count, section: .chatList, text: i18n("Settings.ChatList.Header", lang), badge: nil))
entries.append(.toggle(id: id.count, section: .chatList, settingName: .compactChatList, value: SGSimpleSettings.shared.compactChatList, text: i18n("Settings.CompactChatList", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableChatSwipeOptions, value: !SGSimpleSettings.shared.disableChatSwipeOptions, text: i18n("Settings.ChatSwipeOptions", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableDeleteChatSwipeOption, value: !SGSimpleSettings.shared.disableDeleteChatSwipeOption, text: i18n("Settings.DeleteChatSwipeOption", lang), enabled: !SGSimpleSettings.shared.disableChatSwipeOptions))
entries.append(.header(id: id.count, section: .profiles, text: i18n("Settings.Profiles.Header", lang), badge: nil))
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showProfileId, value: SGSettings.showProfileId, text: i18n("Settings.ShowProfileID", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showDC, value: SGSimpleSettings.shared.showDC, text: i18n("Settings.ShowDC", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showRegDate, value: SGSimpleSettings.shared.showRegDate, text: i18n("Settings.ShowRegDate", lang), enabled: true))
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowRegDate.Notice", lang)))
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showCreationDate, value: SGSimpleSettings.shared.showCreationDate, text: i18n("Settings.ShowCreationDate", lang), enabled: true))
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowCreationDate.Notice", lang)))
entries.append(.toggle(id: id.count, section: .profiles, settingName: .confirmCalls, value: SGSimpleSettings.shared.confirmCalls, text: i18n("Settings.CallConfirmation", lang), enabled: true))
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.CallConfirmation.Notice", lang)))
entries.append(.header(id: id.count, section: .stories, text: presentationData.strings.AutoDownloadSettings_Stories.uppercased(), badge: nil))
entries.append(.toggle(id: id.count, section: .stories, settingName: .hideStories, value: SGSettings.hideStories, text: i18n("Settings.Stories.Hide", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .stories, settingName: .disableSwipeToRecordStory, value: SGSimpleSettings.shared.disableSwipeToRecordStory, text: i18n("Settings.Stories.DisableSwipeToRecord", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .stories, settingName: .warnOnStoriesOpen, value: SGSettings.warnOnStoriesOpen, text: i18n("Settings.Stories.WarnBeforeView", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .stories, settingName: .showRepostToStory, value: SGSimpleSettings.shared.showRepostToStory, text: presentationData.strings.Share_RepostToStory.replacingOccurrences(of: "\n", with: " "), enabled: true))
if SGSimpleSettings.shared.canUseStealthMode {
entries.append(.toggle(id: id.count, section: .stories, settingName: .storyStealthMode, value: SGSimpleSettings.shared.storyStealthMode, text: presentationData.strings.Story_StealthMode_Title, enabled: true))
entries.append(.notice(id: id.count, section: .stories, text: presentationData.strings.Story_StealthMode_ControlText))
} else {
id.increment(2)
}
entries.append(.header(id: id.count, section: .translation, text: presentationData.strings.Localization_TranslateMessages.uppercased(), badge: nil))
entries.append(.toggle(id: id.count, section: .translation, settingName: .quickTranslateButton, value: SGSimpleSettings.shared.quickTranslateButton, text: i18n("Settings.Translation.QuickTranslateButton", lang), enabled: true))
entries.append(.disclosure(id: id.count, section: .translation, link: .languageSettings, text: presentationData.strings.Localization_TranslateEntireChat))
entries.append(.notice(id: id.count, section: .translation, text: i18n("Common.NoTelegramPremiumNeeded", lang, presentationData.strings.Settings_Premium)))
entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.NetworkUsageSettings_MediaImageDataSection, badge: nil))
entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.PhotoEditor_QualityTool.uppercased(), badge: nil))
entries.append(.percentageSlider(id: id.count, section: .photo, settingName: .outgoingPhotoQuality, value: SGSimpleSettings.shared.outgoingPhotoQuality))
entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.Quality.Notice", lang)))
entries.append(.toggle(id: id.count, section: .photo, settingName: .sendLargePhotos, value: SGSimpleSettings.shared.sendLargePhotos, text: i18n("Settings.Photo.SendLarge", lang), enabled: true))
entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.SendLarge.Notice", lang)))
entries.append(.header(id: id.count, section: .stickers, text: presentationData.strings.StickerPacksSettings_Title.uppercased(), badge: nil))
entries.append(.header(id: id.count, section: .stickers, text: i18n("Settings.Stickers.Size", lang), badge: nil))
entries.append(.percentageSlider(id: id.count, section: .stickers, settingName: .stickerSize, value: SGSimpleSettings.shared.stickerSize))
entries.append(.toggle(id: id.count, section: .stickers, settingName: .stickerTimestamp, value: SGSimpleSettings.shared.stickerTimestamp, text: i18n("Settings.Stickers.Timestamp", lang), enabled: true))
entries.append(.header(id: id.count, section: .videoNotes, text: i18n("Settings.VideoNotes.Header", lang), badge: nil))
entries.append(.toggle(id: id.count, section: .videoNotes, settingName: .startTelescopeWithRearCam, value: SGSimpleSettings.shared.startTelescopeWithRearCam, text: i18n("Settings.VideoNotes.StartWithRearCam", lang), enabled: true))
entries.append(.header(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu", lang), badge: nil))
entries.append(.notice(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu.Notice", lang)))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveToCloud, value: SGSimpleSettings.shared.contextShowSaveToCloud, text: i18n("ContextMenu.SaveToCloud", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowHideForwardName, value: SGSimpleSettings.shared.contextShowHideForwardName, text: presentationData.strings.Conversation_ForwardOptions_HideSendersNames, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSelectFromUser, value: SGSimpleSettings.shared.contextShowSelectFromUser, text: i18n("ContextMenu.SelectFromUser", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReport, value: SGSimpleSettings.shared.contextShowReport, text: presentationData.strings.Conversation_ContextMenuReport, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReply, value: SGSimpleSettings.shared.contextShowReply, text: presentationData.strings.Conversation_ContextMenuReply, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowPin, value: SGSimpleSettings.shared.contextShowPin, text: presentationData.strings.Conversation_Pin, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveMedia, value: SGSimpleSettings.shared.contextShowSaveMedia, text: presentationData.strings.Conversation_SaveToFiles, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowMessageReplies, value: SGSimpleSettings.shared.contextShowMessageReplies, text: presentationData.strings.Conversation_ContextViewThread, enabled: true))
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowJson, value: SGSimpleSettings.shared.contextShowJson, text: "JSON", enabled: true))
/* entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan)) */
entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Header", lang), badge: nil))
entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation", lang), badge: nil))
let accountColorSaturation = SGSimpleSettings.shared.accountColorsSaturation
entries.append(.percentageSlider(id: id.count, section: .accountColors, settingName: .accountColorsSaturation, value: accountColorSaturation))
// let nameColor: PeerNameColor
// if let updatedNameColor = state.updatedNameColor {
// nameColor = updatedNameColor
// } else {
// nameColor = .blue
// }
// let _ = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance)
// entries.append(.peerColorPicker(id: entries.count, section: .other,
// colors: nameColors,
// currentColor: nameColor, // TODO: PeerNameColor(rawValue: <#T##Int32#>)
// currentSaturation: accountColorSaturation
// ))
if accountColorSaturation == 0 {
id.increment(100)
entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: presentationData.theme.chat.message.incoming.accentTextColor))
} else {
id.increment(200)
for index in nameColors.displayOrder.prefix(3) {
let color: PeerNameColor = PeerNameColor(rawValue: index)
let colors = nameColors.get(color, dark: presentationData.theme.overallDarkAppearance)
entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: colors.main))
}
}
entries.append(.notice(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation.Notice", lang)))
id.increment(10000)
entries.append(.header(id: id.count, section: .other, text: presentationData.strings.Appearance_Other.uppercased(), badge: nil))
entries.append(.toggle(id: id.count, section: .other, settingName: .swipeForVideoPIP, value: SGSimpleSettings.shared.videoPIPSwipeDirection == SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue, text: i18n("Settings.swipeForVideoPIP", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.swipeForVideoPIP.Notice", lang)))
entries.append(.toggle(id: id.count, section: .other, settingName: .hideChannelBottomButton, value: SGSimpleSettings.shared.hideChannelBottomButton, text: i18n("Settings.hideChannelBottomButton", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .wideChannelPosts, value: SGSimpleSettings.shared.wideChannelPosts, text: i18n("Settings.wideChannelPosts", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .forceBuiltInMic, value: SGSimpleSettings.shared.forceBuiltInMic, text: i18n("Settings.forceBuiltInMic", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.forceBuiltInMic.Notice", lang)))
entries.append(.toggle(id: id.count, section: .other, settingName: .messageDoubleTapActionOutgoingEdit, value: SGSimpleSettings.shared.messageDoubleTapActionOutgoing == SGSimpleSettings.MessageDoubleTapAction.edit.rawValue, text: i18n("Settings.messageDoubleTapActionOutgoingEdit", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .hideRecordingButton, value: !SGSimpleSettings.shared.hideRecordingButton, text: i18n("Settings.RecordingButton", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSnapDeletionEffect, value: !SGSimpleSettings.shared.disableSnapDeletionEffect, text: i18n("Settings.SnapDeletionEffect", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSendAsButton, value: !SGSimpleSettings.shared.disableSendAsButton, text: i18n("Settings.SendAsButton", lang, presentationData.strings.Conversation_SendMesageAs), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableGalleryCamera, value: !SGSimpleSettings.shared.disableGalleryCamera, text: i18n("Settings.GalleryCamera", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextChannel, value: !SGSimpleSettings.shared.disableScrollToNextChannel, text: i18n("Settings.PullToNextChannel", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextTopic, value: !SGSimpleSettings.shared.disableScrollToNextTopic, text: i18n("Settings.PullToNextTopic", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .hideReactions, value: SGSimpleSettings.shared.hideReactions, text: i18n("Settings.HideReactions", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .uploadSpeedBoost, value: SGSimpleSettings.shared.uploadSpeedBoost, text: i18n("Settings.UploadsBoost", lang), enabled: true))
entries.append(.oneFromManySelector(id: id.count, section: .other, settingName: .downloadSpeedBoost, text: i18n("Settings.DownloadsBoost", lang), value: i18n("Settings.DownloadsBoost.\(SGSimpleSettings.shared.downloadSpeedBoost)", lang), enabled: true))
entries.append(.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<SGBoolSetting, SGSliderSetting, SGOneFromManySetting, SGDisclosureLink, AnyHashable>(
context: context,
/*updatePeerColor: { color in
updateState { state in
var updatedState = state
updatedState.updatedNameColor = color
return updatedState
}
},*/ setBoolValue: { setting, value in
switch setting {
case .hidePhoneInSettings:
SGSimpleSettings.shared.hidePhoneInSettings = value
askForRestart?()
case .showTabNames:
SGSimpleSettings.shared.showTabNames = value
askForRestart?()
case .showContactsTab:
let _ = (
updateCallListSettingsInteractively(
accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowContactsTab(value) }
)
).start()
case .showCallsTab:
let _ = (
updateCallListSettingsInteractively(
accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowTab(value) }
)
).start()
case .foldersAtBottom:
let _ = (
updateExperimentalUISettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
var settings = settings
settings.foldersTabAtBottom = value
return settings
}
)
).start()
case .startTelescopeWithRearCam:
SGSimpleSettings.shared.startTelescopeWithRearCam = value
case .hideStories:
let _ = (
updateSGUISettings(engine: context.engine, { settings in
var settings = settings
settings.hideStories = value
return settings
})
).start()
case .showProfileId:
let _ = (
updateSGUISettings(engine: context.engine, { settings in
var settings = settings
settings.showProfileId = value
return settings
})
).start()
case .warnOnStoriesOpen:
let _ = (
updateSGUISettings(engine: context.engine, { settings in
var settings = settings
settings.warnOnStoriesOpen = value
return settings
})
).start()
case .sendWithReturnKey:
let _ = (
updateSGUISettings(engine: context.engine, { settings in
var settings = settings
settings.sendWithReturnKey = value
return settings
})
).start()
case .rememberLastFolder:
SGSimpleSettings.shared.rememberLastFolder = value
case .sendLargePhotos:
SGSimpleSettings.shared.sendLargePhotos = value
case .storyStealthMode:
SGSimpleSettings.shared.storyStealthMode = value
case .disableSwipeToRecordStory:
SGSimpleSettings.shared.disableSwipeToRecordStory = value
case .quickTranslateButton:
SGSimpleSettings.shared.quickTranslateButton = value
case .uploadSpeedBoost:
SGSimpleSettings.shared.uploadSpeedBoost = value
case .hideReactions:
SGSimpleSettings.shared.hideReactions = value
case .showRepostToStory:
SGSimpleSettings.shared.showRepostToStory = value
case .contextShowSelectFromUser:
SGSimpleSettings.shared.contextShowSelectFromUser = value
case .contextShowSaveToCloud:
SGSimpleSettings.shared.contextShowSaveToCloud = value
case .contextShowRestrict:
SGSimpleSettings.shared.contextShowRestrict = value
case .contextShowHideForwardName:
SGSimpleSettings.shared.contextShowHideForwardName = value
case .disableScrollToNextChannel:
SGSimpleSettings.shared.disableScrollToNextChannel = !value
case .disableScrollToNextTopic:
SGSimpleSettings.shared.disableScrollToNextTopic = !value
case .disableChatSwipeOptions:
SGSimpleSettings.shared.disableChatSwipeOptions = !value
simplePromise.set(true) // Trigger update for 'enabled' field of other toggles
askForRestart?()
case .disableDeleteChatSwipeOption:
SGSimpleSettings.shared.disableDeleteChatSwipeOption = !value
askForRestart?()
case .disableGalleryCamera:
SGSimpleSettings.shared.disableGalleryCamera = !value
case .disableSendAsButton:
SGSimpleSettings.shared.disableSendAsButton = !value
case .disableSnapDeletionEffect:
SGSimpleSettings.shared.disableSnapDeletionEffect = !value
case .contextShowReport:
SGSimpleSettings.shared.contextShowReport = value
case .contextShowReply:
SGSimpleSettings.shared.contextShowReply = value
case .contextShowPin:
SGSimpleSettings.shared.contextShowPin = value
case .contextShowSaveMedia:
SGSimpleSettings.shared.contextShowSaveMedia = value
case .contextShowMessageReplies:
SGSimpleSettings.shared.contextShowMessageReplies = value
case .stickerTimestamp:
SGSimpleSettings.shared.stickerTimestamp = value
case .contextShowJson:
SGSimpleSettings.shared.contextShowJson = value
case .hideRecordingButton:
SGSimpleSettings.shared.hideRecordingButton = !value
case .hideTabBar:
SGSimpleSettings.shared.hideTabBar = value
simplePromise.set(true) // Trigger update for 'enabled' field of other toggles
askForRestart?()
case .showDC:
SGSimpleSettings.shared.showDC = value
case .showCreationDate:
SGSimpleSettings.shared.showCreationDate = value
case .showRegDate:
SGSimpleSettings.shared.showRegDate = value
case .compactChatList:
SGSimpleSettings.shared.compactChatList = value
askForRestart?()
case .compactFolderNames:
SGSimpleSettings.shared.compactFolderNames = value
case .allChatsHidden:
SGSimpleSettings.shared.allChatsHidden = value
askForRestart?()
case .defaultEmojisFirst:
SGSimpleSettings.shared.defaultEmojisFirst = value
case .messageDoubleTapActionOutgoingEdit:
SGSimpleSettings.shared.messageDoubleTapActionOutgoing = value ? SGSimpleSettings.MessageDoubleTapAction.edit.rawValue : SGSimpleSettings.MessageDoubleTapAction.default.rawValue
case .wideChannelPosts:
SGSimpleSettings.shared.wideChannelPosts = value
case .forceEmojiTab:
SGSimpleSettings.shared.forceEmojiTab = value
case .forceBuiltInMic:
SGSimpleSettings.shared.forceBuiltInMic = value
case .hideChannelBottomButton:
SGSimpleSettings.shared.hideChannelBottomButton = value
case .confirmCalls:
SGSimpleSettings.shared.confirmCalls = value
case .swipeForVideoPIP:
SGSimpleSettings.shared.videoPIPSwipeDirection = value ? SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue : SGSimpleSettings.VideoPIPSwipeDirection.none.rawValue
}
}, updateSliderValue: { setting, value in
switch (setting) {
case .accountColorsSaturation:
if SGSimpleSettings.shared.accountColorsSaturation != value {
SGSimpleSettings.shared.accountColorsSaturation = value
simplePromise.set(true)
}
case .outgoingPhotoQuality:
if SGSimpleSettings.shared.outgoingPhotoQuality != value {
SGSimpleSettings.shared.outgoingPhotoQuality = value
simplePromise.set(true)
}
case .stickerSize:
if SGSimpleSettings.shared.stickerSize != value {
SGSimpleSettings.shared.stickerSize = value
simplePromise.set(true)
}
}
}, setOneFromManyValue: { setting in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
switch (setting) {
case .downloadSpeedBoost:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.downloadSpeedBoost = value
let enableDownloadX: Bool
switch (value) {
case SGSimpleSettings.DownloadSpeedBoostValues.none.rawValue:
enableDownloadX = false
default:
enableDownloadX = true
}
// Updating controller
simplePromise.set(true)
let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in
var settings = settings
settings.useExperimentalDownload = enableDownloadX
return settings
}).start(completed: {
Queue.mainQueue().async {
askForRestart?()
}
})
}
for value in SGSimpleSettings.DownloadSpeedBoostValues.allCases {
items.append(ActionSheetButtonItem(title: i18n("Settings.DownloadsBoost.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
case .bottomTabStyle:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.bottomTabStyle = value
simplePromise.set(true)
}
for value in SGSimpleSettings.BottomTabStyleValues.allCases {
items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.BottomTabStyle.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
case .allChatsTitleLengthOverride:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.allChatsTitleLengthOverride = value
simplePromise.set(true)
}
for value in SGSimpleSettings.AllChatsTitleLengthOverride.allCases {
let title: String
switch (value) {
case SGSimpleSettings.AllChatsTitleLengthOverride.short:
title = "\"\(presentationData.strings.ChatList_Tabs_All)\""
case SGSimpleSettings.AllChatsTitleLengthOverride.long:
title = "\"\(presentationData.strings.ChatList_Tabs_AllChats)\""
default:
title = i18n("Settings.Folders.AllChatsTitle.none", presentationData.strings.baseLanguageCode)
}
items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
// case .allChatsFolderPositionOverride:
// let setAction: (String) -> Void = { value in
// SGSimpleSettings.shared.allChatsFolderPositionOverride = value
// simplePromise.set(true)
// }
//
// for value in SGSimpleSettings.AllChatsFolderPositionOverride.allCases {
// items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.AllChatsTitle.\(value)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// setAction(value.rawValue)
// }))
// }
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openDisclosureLink: { link in
switch (link) {
case .languageSettings:
pushControllerImpl?(context.sharedContext.makeLocalizationListController(context: context))
case .contentSettings:
let _ = (getSGSettingsURL(context: context) |> deliverOnMainQueue).start(next: { [weak context] url in
guard let strongContext = context else {
return
}
strongContext.sharedContext.applicationBindings.openUrl(url)
})
}
}, searchInput: { searchQuery in
updateState { state in
var updatedState = state
updatedState.searchQuery = searchQuery
return updatedState
}
})
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings])
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings, PreferencesKeys.appConfiguration])
let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network)
|> map(Optional.init)
let contentSettingsConfiguration = Promise<ContentSettingsConfiguration?>()
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
}

View File

@@ -0,0 +1,9 @@
filegroup(
name = "SGShowMessageJson",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@@ -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
}

View File

@@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGSimpleSettings",
module_name = "SGSimpleSettings",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@@ -0,0 +1,58 @@
//// A copy of Atomic from SwiftSignalKit
//import Foundation
//
//public enum AtomicWrapperLockError: Error {
// case isLocked
//}
//
//public final class AtomicWrapper<T> {
// 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<R>(_ f: (T) -> R) -> R {
// pthread_mutex_lock(&self.lock)
// let result = f(self.value)
// pthread_mutex_unlock(&self.lock)
//
// return result
// }
//
// public func tryWith<R>(_ 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
// }
//}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,445 @@
import Foundation
public class SGSimpleSettings {
public static let shared = SGSimpleSettings()
private init() {
setDefaultValues()
preCacheValues()
}
private func setDefaultValues() {
UserDefaults.standard.register(defaults: SGSimpleSettings.defaultValues)
}
private func preCacheValues() {
// let dispatchGroup = DispatchGroup()
let tasks = [
// { let _ = self.allChatsFolderPositionOverride },
{ let _ = self.allChatsHidden },
{ let _ = self.hideTabBar },
{ let _ = self.bottomTabStyle },
{ let _ = self.compactChatList },
{ let _ = self.compactFolderNames },
{ let _ = self.disableSwipeToRecordStory },
{ let _ = self.rememberLastFolder },
{ let _ = self.quickTranslateButton },
{ let _ = self.stickerSize },
{ let _ = self.stickerTimestamp },
{ let _ = self.hideReactions },
{ let _ = self.disableGalleryCamera },
{ let _ = self.disableSendAsButton },
{ let _ = self.disableSnapDeletionEffect },
{ let _ = self.startTelescopeWithRearCam },
{ let _ = self.hideRecordingButton }
]
tasks.forEach { task in
DispatchQueue.global(qos: .background).async(/*group: dispatchGroup*/) {
task()
}
}
// dispatchGroup.notify(queue: DispatchQueue.main) {}
}
public enum Keys: String, CaseIterable {
case hidePhoneInSettings
case showTabNames
case startTelescopeWithRearCam
case accountColorsSaturation
case uploadSpeedBoost
case downloadSpeedBoost
case bottomTabStyle
case rememberLastFolder
case lastAccountFolders
case localDNSForProxyHost
case sendLargePhotos
case outgoingPhotoQuality
case storyStealthMode
case canUseStealthMode
case disableSwipeToRecordStory
case quickTranslateButton
case outgoingLanguageTranslation
case hideReactions
case showRepostToStory
case contextShowSelectFromUser
case contextShowSaveToCloud
case contextShowRestrict
// case contextShowBan
case contextShowHideForwardName
case contextShowReport
case contextShowReply
case contextShowPin
case contextShowSaveMedia
case contextShowMessageReplies
case contextShowJson
case disableScrollToNextChannel
case disableScrollToNextTopic
case disableChatSwipeOptions
case disableDeleteChatSwipeOption
case disableGalleryCamera
case disableSendAsButton
case disableSnapDeletionEffect
case stickerSize
case stickerTimestamp
case hideRecordingButton
case hideTabBar
case showDC
case showCreationDate
case showRegDate
case regDateCache
case compactChatList
case compactFolderNames
case allChatsTitleLengthOverride
// case allChatsFolderPositionOverride
case allChatsHidden
case defaultEmojisFirst
case messageDoubleTapActionOutgoing
case wideChannelPosts
case forceEmojiTab
case forceBuiltInMic
case hideChannelBottomButton
case forceSystemSharing
case confirmCalls
case videoPIPSwipeDirection
case legacyNotificationsFix
case messageFilterKeywords
}
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 static let defaultValues: [String: Any] = [
Keys.hidePhoneInSettings.rawValue: true,
Keys.showTabNames.rawValue: true,
Keys.startTelescopeWithRearCam.rawValue: false,
Keys.accountColorsSaturation.rawValue: 100,
Keys.uploadSpeedBoost.rawValue: false,
Keys.downloadSpeedBoost.rawValue: DownloadSpeedBoostValues.none.rawValue,
Keys.rememberLastFolder.rawValue: false,
Keys.bottomTabStyle.rawValue: BottomTabStyleValues.telegram.rawValue,
Keys.lastAccountFolders.rawValue: [:],
Keys.localDNSForProxyHost.rawValue: false,
Keys.sendLargePhotos.rawValue: false,
Keys.outgoingPhotoQuality.rawValue: 70,
Keys.storyStealthMode.rawValue: false,
Keys.canUseStealthMode.rawValue: true,
Keys.disableSwipeToRecordStory.rawValue: false,
Keys.quickTranslateButton.rawValue: false,
Keys.outgoingLanguageTranslation.rawValue: [:],
Keys.hideReactions.rawValue: false,
Keys.showRepostToStory.rawValue: true,
Keys.contextShowSelectFromUser.rawValue: true,
Keys.contextShowSaveToCloud.rawValue: true,
Keys.contextShowRestrict.rawValue: true,
// Keys.contextShowBan.rawValue: true,
Keys.contextShowHideForwardName.rawValue: true,
Keys.contextShowReport.rawValue: true,
Keys.contextShowReply.rawValue: true,
Keys.contextShowPin.rawValue: true,
Keys.contextShowSaveMedia.rawValue: true,
Keys.contextShowMessageReplies.rawValue: true,
Keys.contextShowJson.rawValue: false,
Keys.disableScrollToNextChannel.rawValue: false,
Keys.disableScrollToNextTopic.rawValue: false,
Keys.disableChatSwipeOptions.rawValue: false,
Keys.disableDeleteChatSwipeOption.rawValue: false,
Keys.disableGalleryCamera.rawValue: false,
Keys.disableSendAsButton.rawValue: false,
Keys.disableSnapDeletionEffect.rawValue: false,
Keys.stickerSize.rawValue: 100,
Keys.stickerTimestamp.rawValue: true,
Keys.hideRecordingButton.rawValue: false,
Keys.hideTabBar.rawValue: false,
Keys.showDC.rawValue: false,
Keys.showCreationDate.rawValue: true,
Keys.showRegDate.rawValue: true,
Keys.regDateCache.rawValue: [:],
Keys.compactChatList.rawValue: false,
Keys.compactFolderNames.rawValue: false,
Keys.allChatsTitleLengthOverride.rawValue: AllChatsTitleLengthOverride.none.rawValue,
// Keys.allChatsFolderPositionOverride.rawValue: AllChatsFolderPositionOverride.none.rawValue
Keys.allChatsHidden.rawValue: false,
Keys.defaultEmojisFirst.rawValue: false,
Keys.messageDoubleTapActionOutgoing.rawValue: MessageDoubleTapAction.default.rawValue,
Keys.wideChannelPosts.rawValue: false,
Keys.forceEmojiTab.rawValue: false,
Keys.hideChannelBottomButton.rawValue: false,
Keys.forceSystemSharing.rawValue: false,
Keys.confirmCalls.rawValue: true,
Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue,
Keys.legacyNotificationsFix.rawValue: false,
Keys.messageFilterKeywords.rawValue: []
]
@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<String, Int32>(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<String, String>(userDefaultsKey: Keys.outgoingLanguageTranslation.rawValue, threadSafe: false)
@UserDefault(key: Keys.hideReactions.rawValue)
public var hideReactions: Bool
@UserDefault(key: Keys.showRepostToStory.rawValue)
public var showRepostToStory: Bool
@UserDefault(key: Keys.contextShowRestrict.rawValue)
public var contextShowRestrict: Bool
/*@UserDefault(key: Keys.contextShowBan.rawValue)
public var contextShowBan: Bool*/
@UserDefault(key: Keys.contextShowSelectFromUser.rawValue)
public var contextShowSelectFromUser: Bool
@UserDefault(key: Keys.contextShowSaveToCloud.rawValue)
public var contextShowSaveToCloud: Bool
@UserDefault(key: Keys.contextShowHideForwardName.rawValue)
public var contextShowHideForwardName: Bool
@UserDefault(key: Keys.contextShowReport.rawValue)
public var contextShowReport: Bool
@UserDefault(key: Keys.contextShowReply.rawValue)
public var contextShowReply: Bool
@UserDefault(key: Keys.contextShowPin.rawValue)
public var contextShowPin: Bool
@UserDefault(key: Keys.contextShowSaveMedia.rawValue)
public var contextShowSaveMedia: Bool
@UserDefault(key: Keys.contextShowMessageReplies.rawValue)
public var contextShowMessageReplies: Bool
@UserDefault(key: Keys.contextShowJson.rawValue)
public var contextShowJson: Bool
@UserDefault(key: Keys.disableScrollToNextChannel.rawValue)
public var disableScrollToNextChannel: Bool
@UserDefault(key: Keys.disableScrollToNextTopic.rawValue)
public var disableScrollToNextTopic: Bool
@UserDefault(key: Keys.disableChatSwipeOptions.rawValue)
public var disableChatSwipeOptions: Bool
@UserDefault(key: Keys.disableDeleteChatSwipeOption.rawValue)
public var disableDeleteChatSwipeOption: Bool
@UserDefault(key: Keys.disableGalleryCamera.rawValue)
public var disableGalleryCamera: Bool
@UserDefault(key: Keys.disableSendAsButton.rawValue)
public var disableSendAsButton: Bool
@UserDefault(key: Keys.disableSnapDeletionEffect.rawValue)
public var disableSnapDeletionEffect: Bool
@UserDefault(key: Keys.stickerSize.rawValue)
public var stickerSize: Int32
@UserDefault(key: Keys.stickerTimestamp.rawValue)
public var stickerTimestamp: Bool
@UserDefault(key: Keys.hideRecordingButton.rawValue)
public var hideRecordingButton: Bool
@UserDefault(key: Keys.hideTabBar.rawValue)
public var hideTabBar: Bool
@UserDefault(key: Keys.showDC.rawValue)
public var showDC: Bool
@UserDefault(key: Keys.showCreationDate.rawValue)
public var showCreationDate: Bool
@UserDefault(key: Keys.showRegDate.rawValue)
public var showRegDate: Bool
public var regDateCache = UserDefaultsBackedDictionary<String, Data>(userDefaultsKey: Keys.regDateCache.rawValue, threadSafe: false)
@UserDefault(key: Keys.compactChatList.rawValue)
public var compactChatList: Bool
@UserDefault(key: Keys.compactFolderNames.rawValue)
public var compactFolderNames: Bool
@UserDefault(key: Keys.allChatsTitleLengthOverride.rawValue)
public var allChatsTitleLengthOverride: String
//
// @UserDefault(key: Keys.allChatsFolderPositionOverride.rawValue)
// public var allChatsFolderPositionOverride: String
@UserDefault(key: Keys.allChatsHidden.rawValue)
public var allChatsHidden: Bool
@UserDefault(key: Keys.defaultEmojisFirst.rawValue)
public var defaultEmojisFirst: Bool
@UserDefault(key: Keys.messageDoubleTapActionOutgoing.rawValue)
public var messageDoubleTapActionOutgoing: String
@UserDefault(key: Keys.wideChannelPosts.rawValue)
public var wideChannelPosts: Bool
@UserDefault(key: Keys.forceEmojiTab.rawValue)
public var forceEmojiTab: Bool
@UserDefault(key: Keys.forceBuiltInMic.rawValue)
public var forceBuiltInMic: Bool
@UserDefault(key: Keys.hideChannelBottomButton.rawValue)
public var hideChannelBottomButton: Bool
@UserDefault(key: Keys.forceSystemSharing.rawValue)
public var forceSystemSharing: Bool
@UserDefault(key: Keys.confirmCalls.rawValue)
public var confirmCalls: Bool
@UserDefault(key: Keys.videoPIPSwipeDirection.rawValue)
public var videoPIPSwipeDirection: String
@UserDefault(key: Keys.legacyNotificationsFix.rawValue)
public var legacyNotificationsFix: Bool
public var b: Bool = true
@UserDefault(key: Keys.messageFilterKeywords.rawValue)
public var messageFilterKeywords: [String]
}
extension SGSimpleSettings {
public var isStealthModeEnabled: Bool {
return storyStealthMode && canUseStealthMode
}
public static func makeOutgoingLanguageTranslationKey(accountId: Int64, peerId: Int64) -> String {
return "\(accountId):\(peerId)"
}
}
public func getSGDownloadPartSize(_ default: Int64, fileSize: Int64?) -> Int64 {
let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost
// Increasing chunk size for small files make it worse in terms of overall download performance
let smallFileSizeThreshold = 1 * 1024 * 1024 // 1 MB
switch (currentDownloadSetting) {
case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue:
if let fileSize, fileSize <= smallFileSizeThreshold {
return `default`
}
return 512 * 1024
case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue:
if let fileSize, fileSize <= smallFileSizeThreshold {
return `default`
}
return 1024 * 1024
default:
return `default`
}
}
public func getSGMaxPendingParts(_ default: Int) -> Int {
let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost
switch (currentDownloadSetting) {
case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue:
return 8
case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue:
return 12
default:
return `default`
}
}
public func sgUseShortAllChatsTitle(_ default: Bool) -> Bool {
let currentOverride = SGSimpleSettings.shared.allChatsTitleLengthOverride
switch (currentOverride) {
case SGSimpleSettings.AllChatsTitleLengthOverride.short.rawValue:
return true
case SGSimpleSettings.AllChatsTitleLengthOverride.long.rawValue:
return false
default:
return `default`
}
}

View File

@@ -0,0 +1,406 @@
import Foundation
public protocol AllowedUserDefaultTypes {}
/* // This one is more painful than helpful
extension Bool: AllowedUserDefaultTypes {}
extension String: AllowedUserDefaultTypes {}
extension Int: AllowedUserDefaultTypes {}
extension Int32: AllowedUserDefaultTypes {}
extension Double: AllowedUserDefaultTypes {}
extension Float: AllowedUserDefaultTypes {}
extension Data: AllowedUserDefaultTypes {}
extension URL: AllowedUserDefaultTypes {}
//extension Dictionary<String, Any>: AllowedUserDefaultTypes {}
extension Array: AllowedUserDefaultTypes where Element: AllowedUserDefaultTypes {}
*/
// Does not support Optional types due to caching
@propertyWrapper
public class UserDefault<T> /*where T: AllowedUserDefaultTypes*/ {
public let key: String
public let userDefaults: UserDefaults
private var cachedValue: T?
public init(key: String, userDefaults: UserDefaults = .standard) {
self.key = key
self.userDefaults = userDefaults
}
public var wrappedValue: T {
get {
#if DEBUG && false
SGtrace("UD.\(key)", what: "GET")
#endif
if let strongCachedValue = cachedValue {
#if DEBUG && false
SGtrace("UD", what: "CACHED \(key) \(strongCachedValue)")
#endif
return strongCachedValue
}
cachedValue = readFromUserDefaults()
#if DEBUG
SGtrace("UD.\(key)", what: "EXTRACTED: \(cachedValue!)")
#endif
return cachedValue!
}
set {
cachedValue = newValue
#if DEBUG
SGtrace("UD.\(key)", what: "CACHE UPDATED \(cachedValue!)")
#endif
userDefaults.set(newValue, forKey: key)
}
}
fileprivate func readFromUserDefaults() -> T {
switch T.self {
case is Bool.Type:
return (userDefaults.bool(forKey: key) as! T)
case is String.Type:
return (userDefaults.string(forKey: key) as! T)
case is Int32.Type:
return (Int32(exactly: userDefaults.integer(forKey: key)) as! T)
case is Int.Type:
return (userDefaults.integer(forKey: key) as! T)
case is Double.Type:
return (userDefaults.double(forKey: key) as! T)
case is Float.Type:
return (userDefaults.float(forKey: key) as! T)
case is Data.Type:
return (userDefaults.data(forKey: key) as! T)
case is URL.Type:
return (userDefaults.url(forKey: key) as! T)
case is Array<String>.Type:
return (userDefaults.stringArray(forKey: key) as! T)
case is Array<Any>.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<T>: UserDefault<T> {
// private let atomicCachedValue: AtomicWrapper<T?> = 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<Key: Hashable, Value> {
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

27
Swiftgram/SGStrings/BUILD Normal file
View File

@@ -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",
],
)

View File

@@ -0,0 +1,121 @@
import Foundation
// Assuming NGLogging and AppBundle are custom modules, they are imported here.
import SGLogging
import AppBundle
public let SGFallbackLocale = "en"
public class SGLocalizationManager {
public static let shared = SGLocalizationManager()
private let appBundle: Bundle
private var localizations: [String: [String: String]] = [:]
private var webLocalizations: [String: [String: String]] = [:]
private let fallbackMappings: [String: String] = [
// "from": "to"
"zh-hant": "zh-hans",
"be": "ru",
"nb": "no",
"ckb": "ku",
"sdh": "ku"
]
private init(fetchLocale: String = SGFallbackLocale) {
self.appBundle = getAppBundle()
// Iterating over all the app languages and loading SGLocalizable.strings
self.appBundle.localizations.forEach { locale in
if locale != "Base" {
localizations[locale] = loadLocalDictionary(for: locale)
}
}
// Downloading one specific locale
self.downloadLocale(fetchLocale)
}
public func localizedString(_ key: String, _ locale: String = SGFallbackLocale, args: CVarArg...) -> String {
let sanitizedLocale = self.sanitizeLocale(locale)
if let localizedString = findLocalizedString(forKey: key, inLocale: sanitizedLocale) {
if args.isEmpty {
return String(format: localizedString)
} else {
return String(format: localizedString, arguments: args)
}
}
SGLogger.shared.log("Strings", "Missing string for key: \(key) in locale: \(locale)")
return key
}
private func loadLocalDictionary(for locale: String) -> [String: String] {
guard let path = self.appBundle.path(forResource: "SGLocalizable", ofType: "strings", inDirectory: nil, forLocalization: locale) else {
// SGLogger.shared.log("Localization", "Unable to find path for locale: \(locale)")
return [:]
}
guard let dictionary = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] else {
// SGLogger.shared.log("Localization", "Unable to load dictionary for locale: \(locale)")
return [:]
}
return dictionary
}
public func downloadLocale(_ locale: String) {
let sanitizedLocale = self.sanitizeLocale(locale)
guard let url = URL(string: self.getStringsUrl(for: sanitizedLocale)) else {
SGLogger.shared.log("Strings", "Invalid URL for locale: \(sanitizedLocale)")
return
}
DispatchQueue.global(qos: .background).async {
if let localeDict = NSDictionary(contentsOf: url) as? [String: String] {
DispatchQueue.main.async {
self.webLocalizations[sanitizedLocale] = localeDict
SGLogger.shared.log("Strings", "Successfully downloaded locale \(sanitizedLocale)")
}
} else {
SGLogger.shared.log("Strings", "Failed to download \(sanitizedLocale)")
}
}
}
private func sanitizeLocale(_ locale: String) -> String {
var sanitizedLocale = locale
let rawSuffix = "-raw"
if locale.hasSuffix(rawSuffix) {
sanitizedLocale = String(locale.dropLast(rawSuffix.count))
}
if sanitizedLocale == "pt-br" {
sanitizedLocale = "pt"
} else if sanitizedLocale == "nb" {
sanitizedLocale = "no"
}
return sanitizedLocale
}
private func findLocalizedString(forKey key: String, inLocale locale: String) -> String? {
if let string = self.webLocalizations[locale]?[key], !string.isEmpty {
return string
}
if let string = self.localizations[locale]?[key], !string.isEmpty {
return string
}
if let fallbackLocale = self.fallbackMappings[locale] {
return self.findLocalizedString(forKey: key, inLocale: fallbackLocale)
}
return self.localizations[SGFallbackLocale]?[key]
}
private func getStringsUrl(for locale: String) -> String {
return "https://raw.githubusercontent.com/Swiftgram/Telegram-iOS/master/Swiftgram/SGStrings/Strings/\(locale).lproj/SGLocalizable.strings"
}
}
public let i18n = SGLocalizationManager.shared.localizedString

View File

@@ -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.";

View File

@@ -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" = "إذا تم تمكينه، سيفتح سحب الفيديو في وضع الصورة في الصورة.";

View File

@@ -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.";

View File

@@ -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.";

View File

@@ -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 Ulæ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.";

View File

@@ -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.";

View File

@@ -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 θα το ανοίξει σε λειτουργία Εικόνα μέσα στην Εικόνα.";

View File

@@ -0,0 +1,152 @@
"Settings.ContentSettings" = "Content Settings";
"Settings.Tabs.Header" = "TABS";
"Settings.Tabs.HideTabBar" = "Hide Tab bar";
"Settings.Tabs.ShowContacts" = "Show Contacts Tab";
"Settings.Tabs.ShowNames" = "Show Tab Names";
"Settings.Folders.BottomTab" = "Folders at Bottom";
"Settings.Folders.BottomTabStyle" = "Bottom Folders Style";
/* Do not translate */
"Settings.Folders.BottomTabStyle.ios" = "iOS";
/* Do not translate */
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
/* Example: Hide "All Chats" */
"Settings.Folders.AllChatsHidden" = "Hide \"%@\"";
"Settings.Folders.RememberLast" = "Open Last Folder";
"Settings.Folders.RememberLast.Notice" = "Swiftgram will open the last used folder when you restart the app or switch accounts.";
"Settings.Folders.CompactNames" = "Smaller spacing";
"Settings.Folders.AllChatsTitle" = "\"All Chats\" title";
"Settings.Folders.AllChatsTitle.short" = "Short";
"Settings.Folders.AllChatsTitle.long" = "Long";
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
"Settings.Folders.AllChatsTitle.none" = "Default";
"Settings.ChatList.Header" = "CHAT LIST";
"Settings.CompactChatList" = "Compact Chat List";
"Settings.Profiles.Header" = "PROFILES";
"Settings.Stories.Hide" = "Hide Stories";
"Settings.Stories.WarnBeforeView" = "Ask Before Viewing";
"Settings.Stories.DisableSwipeToRecord" = "Disable Swipe to Record";
"Settings.Translation.QuickTranslateButton" = "Quick Translate button";
"Stories.Warning.Author" = "Author";
"Stories.Warning.ViewStory" = "View Story?";
/* Author will be able to see that you viewed their Story */
"Stories.Warning.Notice" = "%@ WILL BE ABLE TO SEE that you viewed their Story.";
"Stories.Warning.NoticeStealth" = "%@ will not be able to see that you viewed their Story.";
"Settings.Photo.Quality.Notice" = "Quality of uploaded photos and stories.";
"Settings.Photo.SendLarge" = "Send Large Photos";
"Settings.Photo.SendLarge.Notice" = "Increase the side limit on compressed images to 2560px.";
"Settings.VideoNotes.Header" = "ROUND VIDEOS";
"Settings.VideoNotes.StartWithRearCam" = "Start with Rear Camera";
"Settings.CustomColors.Header" = "ACCOUNT COLORS";
"Settings.CustomColors.Saturation" = "SATURATION";
/* Make sure to escape Percentage sign % */
"Settings.CustomColors.Saturation.Notice" = "Set to 0%% to disable account colors.";
"Settings.UploadsBoost" = "Upload Boost";
"Settings.DownloadsBoost" = "Download Boost";
"Settings.DownloadsBoost.Notice" = "Increases number of parallel connections and size of file chunks. In case your network can't handle the load, try different options that suits your connection.";
"Settings.DownloadsBoost.none" = "Disabled";
"Settings.DownloadsBoost.medium" = "Medium";
"Settings.DownloadsBoost.maximum" = "Maximum";
"Settings.ShowProfileID" = "Show Profile ID";
"Settings.ShowDC" = "Show Data Center";
"Settings.ShowCreationDate" = "Show Chat Creation Date";
"Settings.ShowCreationDate.Notice" = "The creation date may be unknown for some chats.";
"Settings.ShowRegDate" = "Show Registration Date";
"Settings.ShowRegDate.Notice" = "The registration date is approximate.";
"Settings.SendWithReturnKey" = "Send with \"return\" key";
"Settings.HidePhoneInSettingsUI" = "Hide Phone in Settings";
"Settings.HidePhoneInSettingsUI.Notice" = "This will only hide your phone number from the settings interface. To hide it from others, go to Privacy and Security.";
"PasscodeSettings.AutoLock.InFiveSeconds" = "If away for 5 seconds";
"ProxySettings.UseSystemDNS" = "Use system DNS";
"ProxySettings.UseSystemDNS.Notice" = "Use system DNS to bypass timeout if you don't have access to Google DNS";
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
"Common.NoTelegramPremiumNeeded" = "You **don't need** %@!";
"Common.RestartRequired" = "Restart required";
"Common.RestartNow" = "Restart Now";
"Common.OpenTelegram" = "Open Telegram";
"Common.UseTelegramForPremium" = "Please note that to get Telegram Premium, you must use the official Telegram app. Once you have obtained Telegram Premium, all its features will become available in Swiftgram.";
"Message.HoldToShowOrReport" = "Hold to Show or Report.";
"Auth.AccountBackupReminder" = "Make sure you have a backup access method. Keep a SIM for SMS or an additional session logged in to avoid being locked out.";
"Auth.UnofficialAppCodeTitle" = "You can get the code only with official app";
"Settings.SmallReactions" = "Small Reactions";
"Settings.HideReactions" = "Hide Reactions";
"ContextMenu.SaveToCloud" = "Save to Cloud";
"ContextMenu.SelectFromUser" = "Select from Author";
"Settings.ContextMenu" = "CONTEXT MENU";
"Settings.ContextMenu.Notice" = "Disabled entries will be available in \"Swiftgram\" sub-menu.";
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
"Settings.DeleteChatSwipeOption" = "Swipe to Delete Chat";
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
"Settings.PullToNextChannel" = "Pull to Next Unread Channel";
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
"Settings.PullToNextTopic" = "Pull to Next Topic";
"Settings.GalleryCamera" = "Camera in Gallery";
/* "Send Message As..." button */
"Settings.SendAsButton" = "\"%@\" Button";
"Settings.SnapDeletionEffect" = "Message Deletion Effects";
"Settings.Stickers.Size" = "SIZE";
"Settings.Stickers.Timestamp" = "Show Timestamp";
"Settings.RecordingButton" = "Voice Recording Button";
"Settings.DefaultEmojisFirst" = "Prioritize standard emojis";
"Settings.DefaultEmojisFirst.Notice" = "Show standard emojis before premium in emoji keyboard";
/* Date when chat was created. "created: 24 May 2016" */
"Chat.Created" = "created: %@";
/* Date when user joined the chat. "Joined Swiftgram Chat" */
"Chat.JoinedDateTitle" = "Joined %@";
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
"Chat.RegDate" = "Registered";
"Settings.messageDoubleTapActionOutgoingEdit" = "Double-tap to edit message";
"Settings.wideChannelPosts" = "Wide posts in channels";
"Settings.ForceEmojiTab" = "Emoji keyboard by default";
"Settings.forceBuiltInMic" = "Force Device Microphone";
"Settings.forceBuiltInMic.Notice" = "If enabled, app will use only device microphone even if headphones are connected.";
"Settings.hideChannelBottomButton" = "Hide Channel Bottom Panel";
"Settings.CallConfirmation" = "Call Confirmation";
"Settings.CallConfirmation.Notice" = "Swiftgram will ask for your confirmation before making a call.";
/* Confirmation before making a Call */
"CallConfirmation.Audio.Title" = "Make a Call?";
/* Confirmation before making a Video Call */
"CallConfirmation.Video.Title" = "Make a Video Call?";
"MutualContact.Label" = "mutual contact";
"Settings.swipeForVideoPIP" = "Video PIP with Swipe";
"Settings.swipeForVideoPIP.Notice" = "If enabled, swiping video will open it in Picture-in-Picture mode.";

View File

@@ -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.";

View File

@@ -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" = "ذخیره در فضای ابری";

View File

@@ -0,0 +1,152 @@
"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.";
"Message.HoldToShowOrReport" = "Pidä esillä näyttääksesi tai ilmoittaaksesi.";
"Auth.AccountBackupReminder" = "Varmista, että sinulla on varmuuskopio pääsymenetelmästä. Pidä SIM tekstiviestejä varten tai ylimääräinen istunto kirjautuneena välttääksesi lukkiutumisen.";
"Auth.UnofficialAppCodeTitle" = "Koodin voi saada vain virallisella sovelluksella";
"Settings.SmallReactions" = "Pienet reaktiot";
"Settings.HideReactions" = "Piilota reaktiot";
"ContextMenu.SaveToCloud" = "Tallenna Pilveen";
"ContextMenu.SelectFromUser" = "Valitse Tekijältä";
"Settings.ContextMenu" = "KONTEKSTIVALIKKO";
"Settings.ContextMenu.Notice" = "Poistetut kohteet ovat saatavilla 'Swiftgram'-alavalikossa.";
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
"Settings.DeleteChatSwipeOption" = "Vedä poistaaksesi keskustelu";
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
"Settings.PullToNextChannel" = "Vetää seuraavaan lukemattomaan kanavaan";
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
"Settings.PullToNextTopic" = "Vedä seuraava aihe";
"Settings.GalleryCamera" = "Camera in Gallery";
/* "Send Message As..." button */
"Settings.SendAsButton" = "\"%@\" Button";
"Settings.SnapDeletionEffect" = "Message Deletion Effects";
"Settings.Stickers.Size" = "SIZE";
"Settings.Stickers.Timestamp" = "Show Timestamp";
"Settings.RecordingButton" = "Voice Recording Button";
"Settings.DefaultEmojisFirst" = "Anna etusijalle vakiotunnuksia";
"Settings.DefaultEmojisFirst.Notice" = "Näytä vakiotunnukset ennen premium-tunnuksia tunnusnäppäimistössä";
/* Date when chat was created. "created: 24 May 2016" */
"Chat.Created" = "created: %@";
/* Date when user joined the chat. "Joined Swiftgram Chat" */
"Chat.JoinedDateTitle" = "Joined %@";
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
"Chat.RegDate" = "Rekisteröity";
"Settings.messageDoubleTapActionOutgoingEdit" = "Paina kahdesti muokataksesi viestiä";
"Settings.wideChannelPosts" = "Leveitä viestejä kanavissa";
"Settings.ForceEmojiTab" = "Emoji-näppäimistö oletuksena";
"Settings.forceBuiltInMic" = "Pakota laitteen mikrofoni";
"Settings.forceBuiltInMic.Notice" = "Jos otettu käyttöön, sovellus käyttää vain laitteen mikrofonia, vaikka kuulokkeet olisivatkin liitettynä.";
"Settings.hideChannelBottomButton" = "Piilota kanavan alapalkki";
"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.";

View File

@@ -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";

View File

@@ -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" = "אם מופעל, החלקת הווידאו תפתח אותו במצב תמונה בתוך תמונה.";

View File

@@ -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" = "यदि सक्षम है, तो वीडियो को स्वाइप करने से यह चित्र-इन-चित्र मोड में खोला जाएगा।";

View File

@@ -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.";

Some files were not shown because too many files have changed in this diff Show More