Version 11.8.1

This commit is contained in:
Kylmakalle 2024-07-02 19:58:37 +03:00
parent b378b7b282
commit 58cc759146
777 changed files with 39158 additions and 2808 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
@ -13,7 +12,7 @@ url=https://github.com/bazelbuild/rules_swift.git
url = https://github.com/bazelbuild/apple_support.git
[submodule "submodules/TgVoipWebrtc/tgcalls"]
path = submodules/TgVoipWebrtc/tgcalls
url=../tgcalls.git
url=https://github.com/TelegramMessenger/tgcalls.git
[submodule "third-party/libvpx/libvpx"]
path = third-party/libvpx/libvpx
url = https://github.com/webmproject/libvpx.git

View File

@ -1,5 +1,5 @@
{
"sweetpad.build.xcodeWorkspacePath": "Telegram/Telegram.xcodeproj/project.xcworkspace",
"sweetpad.build.xcodeWorkspacePath": "Telegram/Swiftgram.xcodeproj/project.xcworkspace",
"lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
"lldb.launch.expressions": "native"
}

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,188 @@
import Foundation
import SwiftSignalKit
import SGConfig
import SGLogging
import SGSimpleSettings
import SGWebAppExtensions
import SGWebSettingsScheme
import SGRequests
import SGRegDateScheme
private let API_VERSION: String = "0"
private func buildApiUrl(_ endpoint: String) -> String {
return "\(SG_CONFIG.apiUrl)/v\(API_VERSION)/\(endpoint)"
}
public let SG_API_AUTHORIZATION_HEADER = "Authorization"
public let SG_API_DEVICE_TOKEN_HEADER = "Device-Token"
private enum HTTPRequestError {
case network
}
public enum SGAPIError {
case generic(String? = nil)
}
public func getSGSettings(token: String) -> Signal<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()
}
}
}
}
public func postSGReceipt(token: String, deviceToken: String, encodedReceiptData: Data) -> Signal<Void, SGAPIError> {
return Signal { subscriber in
let url = URL(string: buildApiUrl("validate"))!
let headers = [
SG_API_AUTHORIZATION_HEADER: "Token \(token)",
SG_API_DEVICE_TOKEN_HEADER: deviceToken
]
let completed = Atomic<Bool>(value: false)
var request = URLRequest(url: url)
headers.forEach { key, value in
request.addValue(value, forHTTPHeaderField: key)
}
request.httpMethod = "POST"
request.httpBody = encodedReceiptData
let dataSignal = requestsCustom(request: request).start(next: { data, urlResponse in
let _ = completed.swap(true)
if let httpResponse = urlResponse as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200...299:
subscriber.putCompletion()
default:
subscriber.putError(.generic("Error posting Receipt: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "")"))
}
} else {
subscriber.putError(.generic("Not an HTTP response: \(String(describing: urlResponse))"))
}
}, error: { error in
subscriber.putError(.generic("Error posting Receipt: \(String(describing: error))"))
})
return ActionDisposable {
if !completed.with({ $0 }) {
dataSignal.dispose()
}
}
}
}

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

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
}

View File

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

View File

@ -0,0 +1,28 @@
import Foundation
let fallbackBaseBundleId: String = "app.swiftgram.ios"
public func sgAppGroupIdentifier() -> String {
let baseBundleId: String
if let bundleId: String = Bundle.main.bundleIdentifier {
if Bundle.main.bundlePath.hasSuffix(".appex") {
if let lastDotRange: Range<String.Index> = bundleId.range(of: ".", options: [.backwards]) {
baseBundleId = String(bundleId[..<lastDotRange.lowerBound])
} else {
baseBundleId = fallbackBaseBundleId
}
} else {
baseBundleId = bundleId
}
} else {
baseBundleId = fallbackBaseBundleId
}
let result: String = "group.\(baseBundleId)"
#if DEBUG
print("APP_GROUP_IDENTIFIER: \(result)")
#endif
return result
}

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,21 @@
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"
public var iaps: [String] = []
}
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()
}

51
Swiftgram/SGDebugUI/BUILD Normal file
View File

@ -0,0 +1,51 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
config_setting(
name = "debug_build",
values = {
"compilation_mode": "dbg",
},
)
flex_dependency = select({
":debug_build": [
"@flex_sdk//:FLEX"
],
"//conditions:default": [],
})
swift_library(
name = "SGDebugUI",
module_name = "SGDebugUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGItemListUI:SGItemListUI",
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGStrings:SGStrings",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
"//Swiftgram/SGIAP:SGIAP",
"//Swiftgram/SGPayWall:SGPayWall",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/LegacyUI:LegacyUI",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ItemListUI:ItemListUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/AccountContext:AccountContext",
"//submodules/UndoUI:UndoUI"
] + flex_dependency,
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,214 @@
import Foundation
import UniformTypeIdentifiers
import SGItemListUI
import UndoUI
import AccountContext
import Display
import TelegramCore
import Postbox
import ItemListUI
import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
import TelegramUIPreferences
// Optional
import SGSimpleSettings
import SGLogging
import SGPayWall
import OverlayStatusController
#if DEBUG
import FLEX
#endif
private enum SGDebugControllerSection: Int32, SGItemListSection {
case base
case notifications
}
private enum SGDebugDisclosureLink: String {
case sessionBackupManager
case messageFilter
case debugIAP
}
private enum SGDebugActions: String {
case flexing
case fileManager
case clearRegDateCache
case restorePurchases
case resetIAP
}
private enum SGDebugToggles: String {
case forceImmediateShareSheet
case legacyNotificationsFix
case inputToolbar
}
private enum SGDebugOneFromManySetting: String {
case pinnedMessageNotifications
case mentionsAndRepliesNotifications
}
private typealias SGDebugControllerEntry = SGItemListUIEntry<SGDebugControllerSection, SGDebugToggles, AnyHashable, SGDebugOneFromManySetting, 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
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
entries.append(.action(id: id.count, section: .base, actionType: .restorePurchases, text: "PayWall.RestorePurchases".i18n(presentationData.strings.baseLanguageCode), kind: .generic))
entries.append(.action(id: id.count, section: .base, actionType: .resetIAP, text: "Reset Pro", kind: .destructive))
entries.append(.toggle(id: id.count, section: .notifications, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[OLD] Fix empty notifications", enabled: true))
return entries
}
private func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController {
return UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })
}
public func sgDebugController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let simplePromise = ValuePromise(true, ignoreRepeated: false)
let arguments = SGItemListArguments<SGDebugToggles, AnyHashable, SGDebugOneFromManySetting, SGDebugDisclosureLink, SGDebugActions>(context: context, setBoolValue: { toggleName, value in
switch toggleName {
case .forceImmediateShareSheet:
SGSimpleSettings.shared.forceSystemSharing = value
case .legacyNotificationsFix:
SGSimpleSettings.shared.legacyNotificationsFix = value
SGSimpleSettings.shared.synchronizeShared()
case .inputToolbar:
SGSimpleSettings.shared.inputToolbar = value
}
}, setOneFromManyValue: { setting in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
let items: [ActionSheetItem] = []
// var items: [ActionSheetItem] = []
// switch (setting) {
// }
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openDisclosureLink: { _ in
}, action: { actionType in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch actionType {
case .clearRegDateCache:
SGLogger.shared.log("SGDebug", "Regdate cache cleanup init")
/*
let spinner = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(spinner, nil)
*/
SGSimpleSettings.shared.regDateCache.drop()
SGLogger.shared.log("SGDebug", "Regdate cache cleanup succesfull")
presentControllerImpl?(okUndoController("OK: Regdate cache cleaned", presentationData), nil)
/*
Queue.mainQueue().async() { [weak spinner] in
spinner?.dismiss()
}
*/
case .flexing:
#if DEBUG
FLEXManager.shared.toggleExplorer()
#endif
case .fileManager:
#if DEBUG
let baseAppBundleId = Bundle.main.bundleIdentifier!
let appGroupName = "group.\(baseAppBundleId)"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
if let maybeAppGroupUrl = maybeAppGroupUrl {
if let fileManager = FLEXFileBrowserController(path: maybeAppGroupUrl.path) {
FLEXManager.shared.showExplorer()
let flexNavigation = FLEXNavigationController(rootViewController: fileManager)
FLEXManager.shared.presentTool({ return flexNavigation })
}
} else {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Empty path", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
}
#endif
case .restorePurchases:
context.sharedContext.SGIAP?.restorePurchases {
DispatchQueue.main.async {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(args: context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode), timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
}
}
case .resetIAP:
let updateSettingsSignal = updateSGStatusInteractively(accountManager: context.sharedContext.accountManager, { status in
var status = status
status.status = SGStatus.default.status
SGSimpleSettings.shared.primaryUserId = ""
return status
})
let _ = (updateSettingsSignal |> deliverOnMainQueue).start(next: {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Status reset completed. You can now restore purchases.", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
})
}
})
let signal = combineLatest(context.sharedContext.presentationData, simplePromise.get())
|> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = SGDebugControllerEntries(presentationData: presentationData)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ )
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
// Workaround
let _ = pushControllerImpl
return controller
}

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
}

21
Swiftgram/SGIAP/BUILD Normal file
View File

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGIAP",
module_name = "SGIAP",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGConfig:SGConfig",
"//submodules/AppBundle:AppBundle",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,384 @@
import StoreKit
import SGConfig
import SGLogging
import AppBundle
import Combine
private final class CurrencyFormatterEntry {
public let symbol: String
public let thousandsSeparator: String
public let decimalSeparator: String
public let symbolOnLeft: Bool
public let spaceBetweenAmountAndSymbol: Bool
public let decimalDigits: Int
public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) {
self.symbol = symbol
self.thousandsSeparator = thousandsSeparator
self.decimalSeparator = decimalSeparator
self.symbolOnLeft = symbolOnLeft
self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol
self.decimalDigits = decimalDigits
}
}
private func getCurrencyExp(currency: String) -> Int {
switch currency {
case "CLF":
return 4
case "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND":
return 3
case "BIF", "BYR", "CLP", "CVE", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "UYI", "VND", "VUV", "XAF", "XOF", "XPF":
return 0
case "MRO":
return 1
default:
return 2
}
}
private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] {
guard let filePath = getAppBundle().path(forResource: "currencies", ofType: "json") else {
return [:]
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return [:]
}
guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else {
return [:]
}
var result: [String: CurrencyFormatterEntry] = [:]
for (code, contents) in dict {
if let contentsDict = contents as? [String: AnyObject] {
let entry = CurrencyFormatterEntry(
symbol: contentsDict["symbol"] as! String,
thousandsSeparator: contentsDict["thousandsSeparator"] as! String,
decimalSeparator: contentsDict["decimalSeparator"] as! String,
symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue,
spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue,
decimalDigits: getCurrencyExp(currency: code.uppercased())
)
result[code] = entry
result[code.lowercased()] = entry
}
}
return result
}
private let currencyFormatterEntries = loadCurrencyFormatterEntries()
private func fractionalValueToCurrencyAmount(value: Double, currency: String) -> Int64? {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
return nil
}
var factor: Double = 1.0
for _ in 0 ..< entry.decimalDigits {
factor *= 10.0
}
if value > Double(Int64.max) / factor {
return nil
} else {
return Int64(value * factor)
}
}
public extension Notification.Name {
static let SGIAPHelperPurchaseNotification = Notification.Name("SGIAPPurchaseNotification")
static let SGIAPHelperErrorNotification = Notification.Name("SGIAPErrorNotification")
static let SGIAPHelperProductsUpdatedNotification = Notification.Name("SGIAPProductsUpdatedNotification")
static let SGIAPHelperValidationErrorNotification = Notification.Name("SGIAPValidationErrorNotification")
}
public final class SGIAPManager: NSObject {
private var productRequest: SKProductsRequest?
private var productsRequestCompletion: (([SKProduct]) -> Void)?
private var purchaseCompletion: ((Bool, Error?) -> Void)?
public private(set) var availableProducts: [SGProduct] = []
private var finishedSuccessfulTransactions = Set<String>()
private var onRestoreCompletion: (() -> Void)?
public final class SGProduct: Equatable {
private lazy var numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.locale = self.skProduct.priceLocale
return numberFormatter
}()
public let skProduct: SKProduct
init(skProduct: SKProduct) {
self.skProduct = skProduct
}
public var id: String {
return self.skProduct.productIdentifier
}
public var isSubscription: Bool {
if #available(iOS 12.0, *) {
return self.skProduct.subscriptionGroupIdentifier != nil
} else {
return self.skProduct.subscriptionPeriod != nil
}
}
public var price: String {
return self.numberFormatter.string(from: self.skProduct.price) ?? ""
}
public func pricePerMonth(_ monthsCount: Int) -> String {
let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2)
return self.numberFormatter.string(from: price) ?? ""
}
public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String {
let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2)
let prettierPrice = price
.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .up,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
public func multipliedPrice(count: Int) -> String {
let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2)
let prettierPrice = price
.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .up,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
public var priceValue: NSDecimalNumber {
return self.skProduct.price
}
public var priceCurrencyAndAmount: (currency: String, amount: Int64) {
if let currencyCode = self.numberFormatter.currencyCode,
let amount = fractionalValueToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) {
return (currencyCode, amount)
} else {
return ("", 0)
}
}
public static func ==(lhs: SGProduct, rhs: SGProduct) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.isSubscription != rhs.isSubscription {
return false
}
if lhs.priceValue != rhs.priceValue {
return false
}
return true
}
}
public init(foo: Bool = false) { // I don't want to override init, idk why
super.init()
SKPaymentQueue.default().add(self)
#if DEBUG && false
DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
self.requestProducts()
}
#else
self.requestProducts()
#endif
}
deinit {
SKPaymentQueue.default().remove(self)
}
public var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
public func buyProduct(_ product: SKProduct) {
SGLogger.shared.log("SGIAP", "Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
private func requestProducts() {
SGLogger.shared.log("SGIAP", "Requesting products for \(SG_CONFIG.iaps.count) ids...")
let productRequest = SKProductsRequest(productIdentifiers: Set(SG_CONFIG.iaps))
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
public func restorePurchases(completion: @escaping () -> Void) {
SGLogger.shared.log("SGIAP", "Restoring purchases...")
self.onRestoreCompletion = completion
let paymentQueue = SKPaymentQueue.default()
paymentQueue.restoreCompletedTransactions()
}
}
extension SGIAPManager: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.productRequest = nil
DispatchQueue.main.async {
let products = response.products
SGLogger.shared.log("SGIAP", "Received products (\(products.count)): \(products.map({ $0.productIdentifier }).joined(separator: ", "))")
let currentlyAvailableProducts = self.availableProducts
self.availableProducts = products.map({ SGProduct(skProduct: $0) })
if currentlyAvailableProducts != self.availableProducts {
NotificationCenter.default.post(name: .SGIAPHelperProductsUpdatedNotification, object: nil)
}
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
SGLogger.shared.log("SGIAP", "Failed to load list of products. Error \(error.localizedDescription)")
self.productRequest = nil
}
}
extension SGIAPManager: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
SGLogger.shared.log("SGIAP", "paymentQueue transactions \(transactions.count)")
var purchaceTransactions: [SKPaymentTransaction] = []
for transaction in transactions {
SGLogger.shared.log("SGIAP", "Transaction \(transaction.transactionIdentifier ?? "nil") state for product \(transaction.payment.productIdentifier): \(transaction.transactionState.description)")
switch transaction.transactionState {
case .purchased, .restored:
purchaceTransactions.append(transaction)
break
case .purchasing, .deferred:
// Ignoring
break
case .failed:
var localizedError: String = ""
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
localizedError = localizedDescription
SGLogger.shared.log("SGIAP", "Transaction Error [\(transaction.transactionIdentifier ?? "nil")]: \(localizedDescription)")
}
SGLogger.shared.log("SGIAP", "Sending SGIAPHelperErrorNotification for \(transaction.transactionIdentifier ?? "nil")")
NotificationCenter.default.post(name: .SGIAPHelperErrorNotification, object: transaction, userInfo: ["localizedError": localizedError])
default:
SGLogger.shared.log("SGIAP", "Unknown transaction \(transaction.transactionIdentifier ?? "nil") state \(transaction.transactionState). Finishing transaction.")
SKPaymentQueue.default().finishTransaction(transaction)
}
}
if !purchaceTransactions.isEmpty {
SGLogger.shared.log("SGIAP", "Sending SGIAPHelperPurchaseNotification for \(purchaceTransactions.map({ $0.transactionIdentifier ?? "nil" }).joined(separator: ", "))")
NotificationCenter.default.post(name: .SGIAPHelperPurchaseNotification, object: purchaceTransactions)
}
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
SGLogger.shared.log("SGIAP", "Transactions restored")
if let onRestoreCompletion = self.onRestoreCompletion {
self.onRestoreCompletion = nil
onRestoreCompletion()
}
}
}
private extension NSDecimalNumber {
func round(_ decimals: Int) -> NSDecimalNumber {
return self.rounding(accordingToBehavior:
NSDecimalNumberHandler(roundingMode: .down,
scale: Int16(decimals),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false))
}
func prettyPrice() -> NSDecimalNumber {
return self.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .plain,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
}
}
public func getPurchaceReceiptData() -> Data? {
var receiptData: Data?
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
} catch {
SGLogger.shared.log("SGIAP", "Couldn't read receipt data with error: \(error.localizedDescription)")
}
} else {
SGLogger.shared.log("SGIAP", "Couldn't find receipt path")
}
return receiptData
}
extension SKPaymentTransactionState {
var description: String {
switch self {
case .purchasing:
return "Purchasing"
case .purchased:
return "Purchased"
case .failed:
return "Failed"
case .restored:
return "Restored"
case .deferred:
return "Deferred"
@unknown default:
return "Unknown"
}
}
}

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,77 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
import SGConfig
import SGLogging
public struct SGIQTPResponse {
public let status: Int
public let description: String?
public let text: String?
}
public func makeIqtpQuery(_ api: Int, _ method: String, _ args: [String] = []) -> String {
let buildNumber = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] ?? ""
let baseQuery = "tp:\(api):\(buildNumber):\(method)"
if args.isEmpty {
return baseQuery
}
return baseQuery + ":" + args.joined(separator: ":")
}
public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<SGIQTPResponse?, NoError> {
let queryId = arc4random()
#if DEBUG
SGLogger.shared.log("SGIQTP", "[\(queryId)] Query: \(query)")
#else
SGLogger.shared.log("SGIQTP", "[\(queryId)] Query")
#endif
return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to resolve peer \(SG_CONFIG.botUsername)")
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty peer")
return .single(nil)
}
return engine.messages.requestChatContextResults(IQTP: true, botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to request inline results")
return .single(nil)
}
}
|> map { contextResult -> SGIQTPResponse? in
guard let contextResult, let firstResult = contextResult.results.first else {
SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty inline result")
return nil
}
var t: String?
if case let .text(text, _, _, _, _) = firstResult.message {
t = text
}
var status = 400
if let title = firstResult.title {
status = Int(title) ?? 400
}
let response = SGIQTPResponse(
status: status,
description: firstResult.description,
text: t
)
SGLogger.shared.log("SGIQTP", "[\(queryId)] Response: \(response)")
return response
}
}

View File

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

View File

@ -0,0 +1,148 @@
import SwiftUI
import Foundation
// MARK: Swiftgram
@available(iOS 13.0, *)
public struct ChatToolbarView: View {
var onQuote: () -> Void
var onSpoiler: () -> Void
var onBold: () -> Void
var onItalic: () -> Void
var onMonospace: () -> Void
var onLink: () -> Void
var onStrikethrough: () -> Void
var onUnderline: () -> Void
var onCode: () -> Void
var onNewLine: () -> Void
@Binding private var showNewLine: Bool
var onClearFormatting: () -> Void
public init(
onQuote: @escaping () -> Void,
onSpoiler: @escaping () -> Void,
onBold: @escaping () -> Void,
onItalic: @escaping () -> Void,
onMonospace: @escaping () -> Void,
onLink: @escaping () -> Void,
onStrikethrough: @escaping () -> Void,
onUnderline: @escaping () -> Void,
onCode: @escaping () -> Void,
onNewLine: @escaping () -> Void,
showNewLine: Binding<Bool>,
onClearFormatting: @escaping () -> Void
) {
self.onQuote = onQuote
self.onSpoiler = onSpoiler
self.onBold = onBold
self.onItalic = onItalic
self.onMonospace = onMonospace
self.onLink = onLink
self.onStrikethrough = onStrikethrough
self.onUnderline = onUnderline
self.onCode = onCode
self.onNewLine = onNewLine
self._showNewLine = showNewLine
self.onClearFormatting = onClearFormatting
}
public func setShowNewLine(_ value: Bool) {
self.showNewLine = value
}
public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
if showNewLine {
Button(action: onNewLine) {
Image(systemName: "return")
}
.buttonStyle(ToolbarButtonStyle())
}
Button(action: onClearFormatting) {
Image(systemName: "pencil.slash")
}
.buttonStyle(ToolbarButtonStyle())
Spacer()
// Quote Button
Button(action: onQuote) {
Image(systemName: "text.quote")
}
.buttonStyle(ToolbarButtonStyle())
// Spoiler Button
Button(action: onSpoiler) {
Image(systemName: "eye.slash")
}
.buttonStyle(ToolbarButtonStyle())
// Bold Button
Button(action: onBold) {
Image(systemName: "bold")
}
.buttonStyle(ToolbarButtonStyle())
// Italic Button
Button(action: onItalic) {
Image(systemName: "italic")
}
.buttonStyle(ToolbarButtonStyle())
// Monospace Button
Button(action: onMonospace) {
if #available(iOS 16.4, *) {
Text("M").monospaced()
} else {
Text("M")
}
}
.buttonStyle(ToolbarButtonStyle())
// Link Button
Button(action: onLink) {
Image(systemName: "link")
}
.buttonStyle(ToolbarButtonStyle())
// Underline Button
Button(action: onUnderline) {
Image(systemName: "underline")
}
.buttonStyle(ToolbarButtonStyle())
// Strikethrough Button
Button(action: onStrikethrough) {
Image(systemName: "strikethrough")
}
.buttonStyle(ToolbarButtonStyle())
// Code Button
Button(action: onCode) {
Image(systemName: "chevron.left.forwardslash.chevron.right")
}
.buttonStyle(ToolbarButtonStyle())
}
.padding(.horizontal, 8)
.padding(.vertical, 8)
}
.background(Color(UIColor.clear))
}
}
@available(iOS 13.0, *)
struct ToolbarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 17))
.frame(width: 36, height: 36, alignment: .center)
.background(Color(UIColor.tertiarySystemBackground))
.cornerRadius(8)
// TODO(swiftgram): Does not work for fast taps (like mine)
.opacity(configuration.isPressed ? 0.4 : 1.0)
}
}

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

View File

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

View File

@ -0,0 +1,131 @@
import Foundation
import Security
public enum KeychainError: Error {
case duplicateEntry
case unknown(OSStatus)
case itemNotFound
case invalidItemFormat
}
public class KeychainBackupManager {
public static let shared = KeychainBackupManager()
private let service = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
private init() {}
// MARK: - Save Credentials
public func saveSession(id: String, _ session: Data) throws {
// Create query dictionary
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: id,
kSecValueData as String: session,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
// Add to keychain
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
// Item already exists, update it
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: id
]
let attributesToUpdate: [String: Any] = [
kSecValueData as String: session
]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary,
attributesToUpdate as CFDictionary)
if updateStatus != errSecSuccess {
throw KeychainError.unknown(updateStatus)
}
} else if status != errSecSuccess {
throw KeychainError.unknown(status)
}
}
// MARK: - Retrieve Credentials
public func retrieveSession(for id: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: id,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let sessionData = result as? Data else {
throw KeychainError.itemNotFound
}
return sessionData
}
// MARK: - Delete Credentials
public func deleteSession(for id: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: id
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
throw KeychainError.unknown(status)
}
}
// MARK: - Retrieve All Accounts
public func getAllSessons() throws -> [Data] {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return []
}
guard status == errSecSuccess,
let credentialsDataArray = result as? [Data] else {
throw KeychainError.unknown(status)
}
return credentialsDataArray
}
// MARK: - Delete All Sessions
public func deleteAllSessions() throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service
]
let status = SecItemDelete(query as CFDictionary)
// If no items were found, that's fine - just return
if status == errSecItemNotFound {
return
}
// For any other error, throw
if status != errSecSuccess {
throw KeychainError.unknown(status)
}
}
}

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

29
Swiftgram/SGPayWall/BUILD Normal file
View File

@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
filegroup(
name = "SGPayWallAssets",
srcs = glob(["Images.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "SGPayWall",
module_name = "SGPayWall",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGIAP:SGIAP",
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
"//Swiftgram/SGStrings:SGStrings",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
],
visibility = [
"//visibility:public",
],
)

View File

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

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "pro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pro@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pro@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,731 @@
import Foundation
import SwiftUI
import StoreKit
import SGSwiftUI
import SGIAP
import TelegramPresentationData
import LegacyUI
import Display
import SGConfig
import SGStrings
import SwiftSignalKit
import TelegramUIPreferences
@available(iOS 13.0, *)
public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String) -> Void, paymentsEnabled: Bool) -> ViewController {
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let theme = defaultDarkColorPresentationTheme
let strings = presentationData?.strings ?? defaultPresentationStrings
let legacyController = LegacySwiftUIController(
presentation: .modal(animateIn: true),
theme: theme,
strings: strings
)
// legacyController.displayNavigationBar = false
legacyController.statusBar.statusBarStyle = .White
legacyController.attemptNavigation = { _ in return false }
let swiftUIView = SGSwiftUIView<SGPayWallView>(
legacyController: legacyController,
content: {
SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, openUrl: openUrl, paymentsEnabled: paymentsEnabled)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}
private let innerShadowWidth: CGFloat = 15.0
private let accentColorHex: String = "F1552E"
@available(iOS 13.0, *)
struct BackgroundView: View {
var body: some View {
ZStack {
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(hex: "A053F8").opacity(0.8), location: 0.0), // purple gradient
.init(color: Color.clear, location: 0.20),
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.edgesIgnoringSafeArea(.all)
LinearGradient(
gradient: Gradient(stops: [
.init(color: Color(hex: "CC4303").opacity(0.6), location: 0.0), // orange gradient
.init(color: Color.clear, location: 0.15),
]),
startPoint: .topTrailing,
endPoint: .bottomLeading
)
.blendMode(.lighten)
.edgesIgnoringSafeArea(.all)
.overlay(
RoundedRectangle(cornerRadius: 0)
.stroke(Color.clear, lineWidth: 0)
.background(
ZStack {
innerShadow(x: -2, y: -2, blur: 4, color: Color(hex: "FF8C56")) // orange shadow
innerShadow(x: 2, y: 2, blur: 4, color: Color(hex: "A053F8")) // purple shadow
// innerShadow(x: 0, y: 0, blur: 4, color: Color.white.opacity(0.3))
}
)
)
.edgesIgnoringSafeArea(.all)
}
.background(Color.black)
}
func innerShadow(x: CGFloat, y: CGFloat, blur: CGFloat, color: Color) -> some View {
return RoundedRectangle(cornerRadius: 0)
.stroke(color, lineWidth: innerShadowWidth)
.blur(radius: blur)
.offset(x: x, y: y)
.mask(RoundedRectangle(cornerRadius: 0).fill(LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]), startPoint: .top, endPoint: .bottom)))
}
}
@available(iOS 13.0, *)
struct SGPayWallView: View {
@Environment(\.navigationBarHeight) var navigationBarHeight: CGFloat
@Environment(\.containerViewLayout) var containerViewLayout: ContainerViewLayout?
@Environment(\.lang) var lang: String
weak var wrapperController: LegacyController?
let replacementController: ViewController
let SGIAP: SGIAPManager
let statusSignal: Signal<Int64, NoError>
let openUrl: (String) -> Void
let paymentsEnabled: Bool
private enum PayWallState: Equatable {
case ready // ready to buy
case restoring
case purchasing
case validating
}
// State management
@State private var product: SGIAPManager.SGProduct?
@State private var currentStatus: Int64 = 1
@State private var state: PayWallState = .ready
@State private var showErrorAlert: Bool = false
@State private var showConfetti: Bool = false
private let productsPub = NotificationCenter.default.publisher(for: .SGIAPHelperProductsUpdatedNotification, object: nil)
private let buyOrRestoreSuccessPub = NotificationCenter.default.publisher(for: .SGIAPHelperPurchaseNotification, object: nil)
private let buyErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperErrorNotification, object: nil)
private let validationErrorPub = NotificationCenter.default.publisher(for: .SGIAPHelperValidationErrorNotification, object: nil)
@State private var statusTask: Task<Void, Never>? = nil
@State private var hapticFeedback: HapticFeedback?
private let confettiDuration: Double = 5.0
var body: some View {
ZStack {
BackgroundView()
ZStack(alignment: .bottom) {
ScrollView(showsIndicators: false) {
VStack(spacing: 24) {
// Icon
Image("pro")
.frame(width: 100, height: 100)
// Title and Subtitle
VStack(spacing: 8) {
Text("Swiftgram Pro")
.font(.largeTitle)
.fontWeight(.bold)
Text("PayWall.Text".i18n(lang))
.font(.callout)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
// Features
VStack(spacing: 36) {
featuresSection
aboutSection
VStack(spacing: 8) {
HStack {
legalSection
Spacer()
}
HStack {
restorePurchasesButton
Spacer()
}
}
}
// Spacer for purchase buttons
Color.clear.frame(height: 50)
}
.padding(.vertical, 50)
}
.padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout)))
.padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout)))
// Fixed purchase button at bottom
purchaseSection
}
}
.confetti(isActive: $showConfetti, duration: confettiDuration)
.overlay(closeButtonView)
.colorScheme(.dark)
.onReceive(productsPub) { _ in
updateSelectedProduct()
}
.onAppear {
hapticFeedback = HapticFeedback()
updateSelectedProduct()
statusTask = Task {
let statusStream = statusSignal.awaitableStream()
for await newStatus in statusStream {
#if DEBUG
print("SGPayWallView: newStatus = \(newStatus)")
#endif
if Task.isCancelled {
#if DEBUG
print("statusTask cancelled")
#endif
break
}
if currentStatus != newStatus {
currentStatus = newStatus
if newStatus > 1 {
handleUpgradedStatus()
}
}
}
}
}
.onDisappear {
#if DEBUG
print("Cancelling statusTask")
#endif
statusTask?.cancel()
}
.onReceive(buyOrRestoreSuccessPub) { _ in
state = .validating
}
.onReceive(buyErrorPub) { notification in
if let userInfo = notification.userInfo, let error = userInfo["localizedError"] as? String, !error.isEmpty {
showErrorAlert(error)
}
}
.onReceive(validationErrorPub) { notification in
if state == .validating {
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String, !error.isEmpty {
showErrorAlert(error.i18n(lang))
} else {
showErrorAlert("PayWall.ValidationError".i18n(lang))
}
}
}
}
private var featuresSection: some View {
VStack(spacing: 8) {
FeatureRow(
icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue),
title: "PayWall.SessionBackup.Title".i18n(lang),
subtitle: "PayWall.SessionBackup.Notice".i18n(lang)
)
FeatureRow(
icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold),
title: "PayWall.MessageFilter.Title".i18n(lang),
subtitle: "PayWall.MessageFilter.Notice".i18n(lang)
)
FeatureRow(
icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
title: "PayWall.Notifications.Title".i18n(lang),
subtitle: "PayWall.Notifications.Notice".i18n(lang)
)
FeatureRow(
icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
title: "PayWall.InputToolbar.Title".i18n(lang),
subtitle: "PayWall.InputToolbar.Notice".i18n(lang)
)
FeatureRow(
icon: Image("SwiftgramSettings")
.resizable()
.frame(width: 32, height: 32),
title: "PayWall.AppIcons.Title".i18n(lang),
subtitle: "PayWall.AppIcons.Notice".i18n(lang)
)
}
}
private var restorePurchasesButton: some View {
Button(action: handleRestorePurchases) {
Text("PayWall.RestorePurchases".i18n(lang))
.font(.footnote)
.fontWeight(.semibold)
.foregroundColor(Color(hex: accentColorHex))
}
.disabled(state == .restoring || product == nil)
.opacity((state == .restoring || product == nil) ? 0.5 : 1.0)
}
private var purchaseSection: some View {
VStack(spacing: 0) {
Divider()
Button(action: handlePurchase) {
Text(buttonTitle)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(Color(hex: accentColorHex))
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled((state != .ready || !canPurchase) && !(currentStatus > 1))
.opacity(((state != .ready || !canPurchase) && !(currentStatus > 1)) ? 0.5 : 1.0)
.padding([.horizontal, .top])
.padding(.bottom, sgBottomSafeAreaInset(containerViewLayout))
}
.foregroundColor(Color.black)
.backgroundIfAvailable(material: .ultraThinMaterial)
.shadow(radius: 8, y: -4)
}
private var legalSection: some View {
Group {
if #available(iOS 15.0, *) {
Text(LocalizedStringKey("PayWall.Notice.Markdown".i18n(lang, args: "PayWall.TermsURL".i18n(lang), "PayWall.PrivacyURL".i18n(lang))))
.font(.caption)
.tint(Color(hex: accentColorHex))
.foregroundColor(.secondary)
.environment(\.openURL, OpenURLAction { url in
openUrl(url.absoluteString)
return .handled
})
} else {
Text("PayWall.Notice.Raw".i18n(lang))
.font(.caption)
.foregroundColor(.secondary)
HStack(alignment: .top, spacing: 8) {
Button(action: {
openUrl("PayWall.PrivacyURL".i18n(lang))
}) {
Text("PayWall.Privacy".i18n(lang))
.font(.caption)
.foregroundColor(Color(hex: accentColorHex))
}
Button(action: {
openUrl("PayWall.TermsURL".i18n(lang))
}) {
Text("PayWall.Terms".i18n(lang))
.font(.caption)
.foregroundColor(Color(hex: accentColorHex))
}
}
}
}
}
private var aboutSection: some View {
VStack(spacing: 8) {
HStack {
Text("PayWall.About.Title".i18n(lang))
.font(.headline)
.fontWeight(.medium)
Spacer()
}
HStack {
Text("PayWall.About.Notice".i18n(lang))
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
HStack {
Button(action: {
openUrl("PayWall.About.SignatureURL".i18n(lang))
}) {
Text("PayWall.About.Signature".i18n(lang))
.font(.caption)
.foregroundColor(Color(hex: accentColorHex))
}
Spacer()
}
}
}
private var closeButtonView: some View {
Button(action: {
wrapperController?.dismiss(animated: true)
}) {
Image(systemName: "xmark")
.font(.headline)
.foregroundColor(.secondary.opacity(0.6))
.frame(width: 44, height: 44)
.contentShape(Rectangle()) // Improve tappable area
}
.padding([.top, .trailing], 16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
private var buttonTitle: String {
if currentStatus > 1 {
return "PayWall.Button.OpenPro".i18n(lang)
} else {
if state == .purchasing {
return "PayWall.Button.Purchasing".i18n(lang)
} else if state == .restoring {
return "PayWall.Button.Restoring".i18n(lang)
} else if state == .validating {
return "PayWall.Button.Validating".i18n(lang)
} else if let product = product {
if !SGIAP.canMakePayments || paymentsEnabled == false {
return "PayWall.Button.PaymentsUnavailable".i18n(lang)
} else {
return "PayWall.Button.Subscribe".i18n(lang, args: product.price)
}
} else {
return "PayWall.Button.ContactingAppStore".i18n(lang)
}
}
}
private var canPurchase: Bool {
if !SGIAP.canMakePayments || paymentsEnabled == false {
return false
} else {
return product != nil
}
}
private func updateSelectedProduct() {
product = SGIAP.availableProducts.first { $0.id == SG_CONFIG.iaps.first ?? "" }
}
private func handlePurchase() {
if currentStatus > 1 {
wrapperController?.replace(with: replacementController)
} else {
guard let product = product else { return }
state = .purchasing
SGIAP.buyProduct(product.skProduct)
}
}
private func handleRestorePurchases() {
state = .restoring
SGIAP.restorePurchases {
state = .validating
}
}
private func handleUpgradedStatus() {
DispatchQueue.main.async {
hapticFeedback?.success()
showConfetti = true
DispatchQueue.main.asyncAfter(deadline: .now() + confettiDuration + 1.0) {
showConfetti = false
}
}
}
private func showErrorAlert(_ message: String) {
let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
state = .ready
}))
DispatchQueue.main.async {
wrapperController?.present(alertController, animated: true)
}
}
}
@available(iOS 13.0, *)
struct FeatureIcon: View {
let icon: String
let iconColor: Color
let backgroundColor: Color
let iconSize: CGFloat
let frameSize: CGFloat
let fontWeight: SwiftUI.Font.Weight
init(
icon: String,
iconColor: Color = .white,
backgroundColor: Color = .blue,
iconSize: CGFloat = 18,
frameSize: CGFloat = 32,
fontWeight: SwiftUI.Font.Weight = .regular
) {
self.icon = icon
self.iconColor = iconColor
self.backgroundColor = backgroundColor
self.iconSize = iconSize
self.frameSize = frameSize
self.fontWeight = fontWeight
}
var body: some View {
Image(systemName: icon)
.font(.system(size: iconSize))
.fontWeightIfAvailable(fontWeight)
.foregroundColor(iconColor)
.frame(width: frameSize, height: frameSize)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
@available(iOS 13.0, *)
struct FeatureRow<IconContent: View>: View {
let icon: IconContent
let title: String
let subtitle: String
var body: some View {
Button(action: {
// TODO(swiftgram): Feature row clarification
}) {
HStack(spacing: 16) {
HStack(alignment: .top, spacing: 12) {
icon
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.fontWeight(.medium)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Spacer()
// TODO(swiftgram): uncomment
// Image(systemName: "chevron.right")
// .font(.system(size: 12, weight: .semibold))
// .foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.systemGray6))
.shadow(color: .black.opacity(0.05), radius: 8, x: 0, y: 4)
)
}
.buttonStyle(PlainButtonStyle())
}
}
// Confetti
@available(iOS 13.0, *)
struct ConfettiType {
let color: Color
let shape: ConfettiShape
static func random() -> ConfettiType {
let colors: [Color] = [.red, .blue, .green, .yellow, .pink, .purple, .orange]
return ConfettiType(
color: colors.randomElement() ?? .blue,
shape: ConfettiShape.allCases.randomElement() ?? .circle
)
}
}
@available(iOS 13.0, *)
enum ConfettiShape: CaseIterable {
case circle
case triangle
case square
case slimRectangle
case roundedCross
@ViewBuilder
func view(color: Color) -> some View {
switch self {
case .circle:
Circle().fill(color)
case .triangle:
Triangle().fill(color)
case .square:
Rectangle().fill(color)
case .slimRectangle:
SlimRectangle().fill(color)
case .roundedCross:
RoundedCross().fill(color)
}
}
}
@available(iOS 13.0, *)
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.closeSubpath()
return path
}
}
@available(iOS 13.0, *)
public struct SlimRectangle: Shape {
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: 4*rect.maxY/5))
path.addLine(to: CGPoint(x: rect.maxX, y: 4*rect.maxY/5))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
return path
}
}
@available(iOS 13.0, *)
public struct RoundedCross: Shape {
public func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY/3))
path.addQuadCurve(to: CGPoint(x: rect.maxX/3, y: rect.minY), control: CGPoint(x: rect.maxX/3, y: rect.maxY/3))
path.addLine(to: CGPoint(x: 2*rect.maxX/3, y: rect.minY))
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY/3), control: CGPoint(x: 2*rect.maxX/3, y: rect.maxY/3))
path.addLine(to: CGPoint(x: rect.maxX, y: 2*rect.maxY/3))
path.addQuadCurve(to: CGPoint(x: 2*rect.maxX/3, y: rect.maxY), control: CGPoint(x: 2*rect.maxX/3, y: 2*rect.maxY/3))
path.addLine(to: CGPoint(x: rect.maxX/3, y: rect.maxY))
path.addQuadCurve(to: CGPoint(x: 2*rect.minX/3, y: 2*rect.maxY/3), control: CGPoint(x: rect.maxX/3, y: 2*rect.maxY/3))
return path
}
}
@available(iOS 13.0, *)
struct ConfettiModifier: ViewModifier {
@Binding var isActive: Bool
let duration: Double
func body(content: Content) -> some View {
content.overlay(
ZStack {
if isActive {
ForEach(0..<70) { _ in
ConfettiPiece(
confettiType: .random(),
duration: duration
)
}
}
}
)
}
}
@available(iOS 13.0, *)
struct ConfettiPiece: View {
let confettiType: ConfettiType
let duration: Double
@State private var isAnimating = false
@State private var rotation = Double.random(in: 0...1080)
var body: some View {
confettiType.shape.view(color: confettiType.color)
.frame(width: 10, height: 10)
.rotationEffect(.degrees(rotation))
.position(
x: .random(in: 0...UIScreen.main.bounds.width),
y: 0 //-20
)
.modifier(FallingModifier(distance: UIScreen.main.bounds.height + 20, duration: duration))
.opacity(isAnimating ? 0 : 1)
.onAppear {
withAnimation(.linear(duration: duration)) {
isAnimating = true
}
}
}
}
@available(iOS 13.0, *)
struct FallingModifier: ViewModifier {
let distance: CGFloat
let duration: Double
func body(content: Content) -> some View {
content.modifier(
MoveModifier(
offset: CGSize(
width: .random(in: -100...100),
height: distance
),
duration: duration
)
)
}
}
@available(iOS 13.0, *)
struct MoveModifier: ViewModifier {
let offset: CGSize
let duration: Double
@State private var isAnimating = false
func body(content: Content) -> some View {
content.offset(
x: isAnimating ? offset.width : 0,
y: isAnimating ? offset.height : 0
)
.onAppear {
withAnimation(
.linear(duration: duration)
.speed(.random(in: 0.5...2.5))
) {
isAnimating = true
}
}
}
}
// Extension to make it easier to use
@available(iOS 13.0, *)
extension View {
func confetti(isActive: Binding<Bool>, duration: Double = 2.0) -> some View {
modifier(ConfettiModifier(isActive: isActive, duration: duration))
}
}

41
Swiftgram/SGProUI/BUILD Normal file
View File

@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGProUI",
module_name = "SGProUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGKeychainBackupManager:SGKeychainBackupManager",
"//Swiftgram/SGItemListUI:SGItemListUI",
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGStrings:SGStrings",
"//Swiftgram/SGAPI:SGAPI",
"//Swiftgram/SGAPIToken:SGAPIToken",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
#
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/ItemListUI:ItemListUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/UndoUI:UndoUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,181 @@
import Foundation
import SwiftUI
import SGSwiftUI
import SGStrings
import SGSimpleSettings
import LegacyUI
import Display
import TelegramPresentationData
@available(iOS 13.0, *)
struct MessageFilterKeywordInputFieldModifier: ViewModifier {
@Binding var newKeyword: String
let onAdd: () -> Void
func body(content: Content) -> some View {
if #available(iOS 15.0, *) {
content
.submitLabel(.return)
.submitScope(false) // TODO(swiftgram): Keyboard still closing
.interactiveDismissDisabled()
.onSubmit {
onAdd()
}
} else {
content
}
}
}
@available(iOS 13.0, *)
struct MessageFilterKeywordInputView: View {
@Environment(\.lang) var lang: String
@Binding var newKeyword: String
let onAdd: () -> Void
var body: some View {
HStack {
TextField("MessageFilter.InputPlaceholder".i18n(lang), text: $newKeyword)
.autocorrectionDisabled(true)
.autocapitalization(.none)
.keyboardType(.default)
.modifier(MessageFilterKeywordInputFieldModifier(newKeyword: $newKeyword, onAdd: onAdd))
Button(action: onAdd) {
Image(systemName: "plus.circle.fill")
.foregroundColor(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor)
.imageScale(.large)
}
.disabled(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
.buttonStyle(PlainButtonStyle())
}
}
}
@available(iOS 13.0, *)
struct MessageFilterView: View {
weak var wrapperController: LegacyController?
@Environment(\.lang) var lang: String
@State private var newKeyword: String = ""
@State private var keywords: [String] {
didSet {
SGSimpleSettings.shared.messageFilterKeywords = keywords
}
}
init(wrapperController: LegacyController?) {
self.wrapperController = wrapperController
_keywords = State(initialValue: SGSimpleSettings.shared.messageFilterKeywords)
}
var bodyContent: some View {
List {
Section {
// Icon and title
VStack(spacing: 8) {
Image(systemName: "nosign.app.fill")
.font(.system(size: 50))
.foregroundColor(.secondary)
Text("MessageFilter.Title".i18n(lang))
.font(.title)
.bold()
Text("MessageFilter.SubTitle".i18n(lang))
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.listRowInsets(EdgeInsets())
}
Section {
MessageFilterKeywordInputView(newKeyword: $newKeyword, onAdd: addKeyword)
}
Section(header: Text("MessageFilter.Keywords.Title".i18n(lang))) {
ForEach(keywords.reversed(), id: \.self) { keyword in
Text(keyword)
}
.onDelete { indexSet in
let originalIndices = IndexSet(
indexSet.map { keywords.count - 1 - $0 }
)
deleteKeywords(at: originalIndices)
}
}
}
.tgNavigationBackButton(wrapperController: wrapperController)
}
var body: some View {
NavigationView {
if #available(iOS 14.0, *) {
bodyContent
.toolbar {
EditButton()
}
} else {
bodyContent
}
}
}
private func addKeyword() {
let trimmedKeyword = newKeyword.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedKeyword.isEmpty else { return }
let keywordExists = keywords.contains {
$0 == trimmedKeyword
}
guard !keywordExists else {
return
}
withAnimation {
keywords.append(trimmedKeyword)
}
newKeyword = ""
}
private func deleteKeywords(at offsets: IndexSet) {
withAnimation {
keywords.remove(atOffsets: offsets)
}
}
}
@available(iOS 13.0, *)
public func sgMessageFilterController(presentationData: PresentationData? = nil) -> ViewController {
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let strings = presentationData?.strings ?? defaultPresentationStrings
let legacyController = LegacySwiftUIController(
presentation: .navigation,
theme: theme,
strings: strings
)
// Status bar color will break if theme changed
legacyController.statusBar.statusBarStyle = theme.rootController
.statusBarStyle.style
legacyController.displayNavigationBar = false
let swiftUIView = SGSwiftUIView<MessageFilterView>(
legacyController: legacyController,
content: {
MessageFilterView(wrapperController: legacyController)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}

View File

@ -0,0 +1,186 @@
import Foundation
import UniformTypeIdentifiers
import SGItemListUI
import UndoUI
import AccountContext
import Display
import TelegramCore
import Postbox
import ItemListUI
import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
import TelegramUIPreferences
// Optional
import SGSimpleSettings
import SGLogging
private enum SGProControllerSection: Int32, SGItemListSection {
case base
case notifications
case footer
}
private enum SGProDisclosureLink: String {
case sessionBackupManager
case messageFilter
}
private enum SGProToggles: String {
case inputToolbar
}
private enum SGProOneFromManySetting: String {
case pinnedMessageNotifications
case mentionsAndRepliesNotifications
}
private enum SGProAction {
case resetIAP
}
private typealias SGProControllerEntry = SGItemListUIEntry<SGProControllerSection, SGProToggles, AnyHashable, SGProOneFromManySetting, SGProDisclosureLink, SGProAction>
private func SGProControllerEntries(presentationData: PresentationData) -> [SGProControllerEntry] {
var entries: [SGProControllerEntry] = []
let lang = presentationData.strings.baseLanguageCode
let id = SGItemListCounter()
entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "SessionBackup.Title".i18n(lang)))
entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "MessageFilter.Title".i18n(lang)))
entries.append(.toggle(id: id.count, section: .base, settingName: .inputToolbar, value: SGSimpleSettings.shared.inputToolbar, text: "InputToolbar.Title".i18n(lang), enabled: true))
entries.append(.header(id: id.count, section: .notifications, text: presentationData.strings.Notifications_Title.uppercased(), badge: nil))
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .pinnedMessageNotifications, text: "Notifications.PinnedMessages.Title".i18n(lang), value: "Notifications.PinnedMessages.value.\(SGSimpleSettings.shared.pinnedMessageNotifications)".i18n(lang), enabled: true))
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .mentionsAndRepliesNotifications, text: "Notifications.MentionsAndReplies.Title".i18n(lang), value: "Notifications.MentionsAndReplies.value.\(SGSimpleSettings.shared.mentionsAndRepliesNotifications)".i18n(lang), enabled: true))
#if DEBUG
entries.append(.action(id: id.count, section: .footer, actionType: .resetIAP, text: "Reset Pro", kind: .destructive))
#endif
return entries
}
public func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController {
return UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })
}
public func sgProController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let simplePromise = ValuePromise(true, ignoreRepeated: false)
let arguments = SGItemListArguments<SGProToggles, AnyHashable, SGProOneFromManySetting, SGProDisclosureLink, SGProAction>(context: context, setBoolValue: { toggleName, value in
switch toggleName {
case .inputToolbar:
SGSimpleSettings.shared.inputToolbar = value
}
}, setOneFromManyValue: { setting in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
switch (setting) {
case .pinnedMessageNotifications:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.pinnedMessageNotifications = value
SGSimpleSettings.shared.synchronizeShared()
simplePromise.set(true)
}
for value in SGSimpleSettings.PinnedMessageNotificationsSettings.allCases {
items.append(ActionSheetButtonItem(title: "Notifications.PinnedMessages.value.\(value.rawValue)".i18n(lang), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
case .mentionsAndRepliesNotifications:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.mentionsAndRepliesNotifications = value
SGSimpleSettings.shared.synchronizeShared()
simplePromise.set(true)
}
for value in SGSimpleSettings.MentionsAndRepliesNotificationsSettings.allCases {
items.append(ActionSheetButtonItem(title: "Notifications.MentionsAndReplies.value.\(value.rawValue)".i18n(lang), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openDisclosureLink: { link in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch (link) {
case .sessionBackupManager:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
} else {
presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil)
}
case .messageFilter:
if #available(iOS 13.0, *) {
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
} else {
presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil)
}
}
}, action: { action in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch action {
case .resetIAP:
let updateSettingsSignal = updateSGStatusInteractively(accountManager: context.sharedContext.accountManager, { status in
var status = status
status.status = SGStatus.default.status
SGSimpleSettings.shared.primaryUserId = ""
return status
})
let _ = (updateSettingsSignal |> deliverOnMainQueue).start(next: {
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "Status reset completed. You can now restore purchases.", timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
})
}
})
let signal = combineLatest(context.sharedContext.presentationData, simplePromise.get())
|> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = SGProControllerEntries(presentationData: presentationData)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram Pro"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ )
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
// Workaround
let _ = pushControllerImpl
return controller
}

View File

@ -0,0 +1,520 @@
import Foundation
import UndoUI
import AccountContext
import TelegramCore
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData
import PresentationDataUtils
import SGSimpleSettings
import SGLogging
import SGKeychainBackupManager
struct SessionBackup: Codable {
var name: String? = nil
var date: Date = Date()
let accountRecord: AccountRecord<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 {
@Environment(\.lang) var lang: String
let backup: SessionBackup
let isLoggedIn: Bool
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
var formattedDate: String {
if #available(iOS 15.0, *) {
return backup.date.formatted(date: .abbreviated, time: .shortened)
} else {
return dateFormatter.string(from: backup.date)
}
}
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(backup.name ?? String(backup.userId))
.font(.body)
Text("ID: \(backup.userId)")
.font(.subheadline)
.foregroundColor(.secondary)
Text("SessionBackup.LastBackupAt".i18n(lang, args: formattedDate))
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text((isLoggedIn ? "SessionBackup.LoggedIn" : "SessionBackup.LoggedOut").i18n(lang))
.font(.caption)
.foregroundColor(isLoggedIn ? .white : .secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(isLoggedIn ? Color.accentColor : Color.secondary.opacity(0.1))
.cornerRadius(4)
}
.padding(.vertical, 4)
}
}
@available(iOS 13.0, *)
struct BorderedButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.accentColor, lineWidth: 1)
)
.opacity(configuration.isPressed ? 0.7 : 1.0)
}
}
@available(iOS 13.0, *)
struct SessionBackupManagerView: View {
@Environment(\.lang) var lang: String
weak var wrapperController: LegacyController?
let context: AccountContext
@State private var sessions: [SessionBackup] = []
@State private var loggedInPeerIDs: [Int64] = []
@State private var loggedInAccountsDisposable: Disposable? = nil
private func performBackup() {
let controller = OverlayStatusController(theme: context.sharedContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil))
let signal = context.sharedContext.accountManager.accountRecords()
|> take(1)
|> deliverOnMainQueue
let signal2 = context.sharedContext.activeAccountsWithInfo
|> take(1)
|> deliverOnMainQueue
wrapperController?.present(controller, in: .window(.root), with: nil)
Task {
if let result = try? await combineLatest(signal, signal2).awaitable() {
let (view, accountsWithInfo) = result
backupSessionsFromView(view, accountsWithInfo: accountsWithInfo.1)
withAnimation {
sessions = getBackedSessions()
}
controller.dismiss()
}
}
}
private func performRestore() {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
let _ = (context.sharedContext.accountManager.accountRecords()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] view in
let backupSessions = getBackedSessions()
var restoredSessions: Int64 = 0
func importNextBackup(index: Int) {
// Check if we're done
if index >= backupSessions.count {
// All done, update UI
withAnimation {
sessions = getBackedSessions()
}
controller?.dismiss()
wrapperController?.present(
okUndoController("SessionBackup.RestoreOK".i18n(lang, args: "\(restoredSessions)"), presentationData),
in: .current
)
return
}
let backup = backupSessions[index]
// Check for existing record
let existingRecord = view.records.first { record in
var userId: Int64 = 0
for attribute in record.attributes {
if case let .backupData(backupData) = attribute {
userId = backupData.data?.peerId ?? 0
}
}
return userId == backup.peerIdInternal
}
if existingRecord != nil {
SGLogger.shared.log("SessionBackup", "Record \(backup.userId) already exists, skipping")
importNextBackup(index: index + 1)
return
}
var importAttributes = backup.accountRecord.attributes
importAttributes.removeAll { attribute in
if case .sortOrder = attribute {
return true
}
return false
}
let importBackupSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in
for attribute in record.attributes {
if case let .sortOrder(sortOrder) = attribute {
return sortOrder.order
}
}
return 0
}).max() ?? 0) + 1
importAttributes.append(.sortOrder(AccountSortOrderAttribute(order: nextSortOrder)))
let accountRecordId = transaction.createRecord(importAttributes)
SGLogger.shared.log("SessionBackup", "Imported record \(accountRecordId) for \(backup.userId)")
restoredSessions += 1
}
|> deliverOnMainQueue
let _ = importBackupSignal.start(completed: {
importNextBackup(index: index + 1)
})
}
// Start the import chain
importNextBackup(index: 0)
})
wrapperController?.present(controller, in: .window(.root), with: nil)
}
private func performDeleteAll() {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = textAlertController(context: context, title: "SessionBackup.DeleteAllBackups".i18n(lang), text: "SessionBackup.DeleteAllBackups.Subtitle".i18n(lang), actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
wrapperController?.present(controller, in: .window(.root), with: nil)
do {
try KeychainBackupManager.shared.deleteAllSessions()
withAnimation {
sessions = getBackedSessions()
}
controller.dismiss()
} catch let e {
SGLogger.shared.log("SessionBackup", "Error deleting all sessions: \(e)")
}
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
])
wrapperController?.present(controller, in: .window(.root), with: nil)
}
private func performDelete(_ session: SessionBackup) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = textAlertController(context: context, title: "SessionBackup.DeleteSingle.Title".i18n(lang), text: "SessionBackup.DeleteSingle.Text".i18n(lang, args: "\(session.name ?? "\(session.userId)")"), actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
wrapperController?.present(controller, in: .window(.root), with: nil)
do {
try KeychainBackupManager.shared.deleteSession(for: "\(session.peerIdInternal)")
withAnimation {
sessions = getBackedSessions()
}
controller.dismiss()
} catch let e {
SGLogger.shared.log("SessionBackup", "Error deleting session: \(e)")
}
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
])
wrapperController?.present(controller, in: .window(.root), with: nil)
}
private func performRemoveSessionFromApp(session: SessionBackup) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = textAlertController(context: context, title: "SessionBackup.RemoveFromApp.Title".i18n(lang), text: "SessionBackup.RemoveFromApp.Text".i18n(lang, args: "\(session.name ?? "\(session.userId)")"), actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
wrapperController?.present(controller, in: .window(.root), with: nil)
let signal = context.sharedContext.accountManager.accountRecords()
|> take(1)
|> deliverOnMainQueue
let _ = signal.start(next: { [weak controller] view in
// Find record to delete
let accountRecord = view.records.first { record in
var userId: Int64 = 0
for attribute in record.attributes {
if case let .backupData(backupData) = attribute {
userId = backupData.data?.peerId ?? 0
}
}
return userId == session.peerIdInternal
}
if let record = accountRecord {
let deleteSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
transaction.updateRecord(record.id, { _ in return nil})
}
|> deliverOnMainQueue
let _ = deleteSignal.start(next: {
withAnimation {
sessions = getBackedSessions()
}
controller?.dismiss()
})
} else {
controller?.dismiss()
}
})
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
])
wrapperController?.present(controller, in: .window(.root), with: nil)
}
var body: some View {
List {
Section() {
Button(action: performBackup) {
HStack {
Image(systemName: "key.fill")
.frame(width: 30)
Text("SessionBackup.Actions.Backup".i18n(lang))
Spacer()
}
}
Button(action: performRestore) {
HStack {
Image(systemName: "arrow.2.circlepath")
.frame(width: 30)
Text("SessionBackup.Actions.Restore".i18n(lang))
Spacer()
}
}
Button(action: performDeleteAll) {
HStack {
Image(systemName: "trash")
.frame(width: 30)
Text("SessionBackup.Actions.DeleteAll".i18n(lang))
}
}
.foregroundColor(.red)
}
Text("SessionBackup.Notice".i18n(lang))
.font(.caption)
.foregroundColor(.secondary)
Section(header: Text("SessionBackup.Sessions.Title".i18n(lang))) {
ForEach(sessions, id: \.peerIdInternal) { session in
SessionBackupRow(
backup: session,
isLoggedIn: loggedInPeerIDs.contains(session.peerIdInternal)
)
.contextMenu {
Button(action: {
performDelete(session)
}, label: {
HStack(spacing: 4) {
Text("SessionBackup.Actions.DeleteOne".i18n(lang))
Image(systemName: "trash")
}
})
Button(action: {
performRemoveSessionFromApp(session: session)
}, label: {
HStack(spacing: 4) {
Text("SessionBackup.Actions.RemoveFromApp".i18n(lang))
Image(systemName: "trash")
}
})
}
}
// .onDelete { indexSet in
// performDelete(indexSet)
// }
}
}
.onAppear {
withAnimation {
sessions = getBackedSessions()
}
let accountsSignal = context.sharedContext.accountManager.accountRecords()
|> deliverOnMainQueue
loggedInAccountsDisposable = accountsSignal.start(next: { view in
var result: [Int64] = []
for record in view.records {
var isLoggedOut: Bool = false
var userId: Int64 = 0
for attribute in record.attributes {
if case .loggedOut = attribute {
isLoggedOut = true
} else if case let .backupData(backupData) = attribute {
userId = backupData.data?.peerId ?? 0
}
}
if !isLoggedOut && userId != 0 {
result.append(userId)
}
}
SGLogger.shared.log("SessionBackup", "Logged in accounts: \(result)")
if loggedInPeerIDs != result {
SGLogger.shared.log("SessionBackup", "Updating logged in accounts: \(result)")
loggedInPeerIDs = result
}
})
}
.onDisappear {
loggedInAccountsDisposable?.dispose()
}
}
}
func getBackedSessions() -> [SessionBackup] {
var sessions: [SessionBackup] = []
do {
let backupSessionsData = try KeychainBackupManager.shared.getAllSessons()
for sessionBackupData in backupSessionsData {
do {
let backup = try JSONDecoder().decode(SessionBackup.self, from: sessionBackupData)
sessions.append(backup)
} catch let e {
SGLogger.shared.log("SessionBackup", "IMPORT ERROR: \(e)")
}
}
} catch let e {
SGLogger.shared.log("SessionBackup", "Error getting all sessions: \(e)")
}
return sessions
}
func backupSessionsFromView(_ view: AccountRecordsView<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 {
SGLogger.shared.log("SessionBackup", "BACKUP ERROR: \(e)")
}
}
}
@available(iOS 13.0, *)
public func sgSessionBackupManagerController(context: AccountContext, presentationData: PresentationData? = nil) -> ViewController {
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
let strings = presentationData?.strings ?? defaultPresentationStrings
let legacyController = LegacySwiftUIController(
presentation: .navigation,
theme: theme,
strings: strings
)
legacyController.statusBar.statusBarStyle = theme.rootController
.statusBarStyle.style
legacyController.title = "SessionBackup.Title".i18n(strings.baseLanguageCode)
let swiftUIView = SGSwiftUIView<SessionBackupManagerView>(
legacyController: legacyController,
manageSafeArea: true,
content: {
SessionBackupManagerView(wrapperController: legacyController, context: context)
}
)
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
legacyController.bind(controller: controller)
return legacyController
}

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" : "SwiftgramPro.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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,681 @@
// MARK: Swiftgram
import SGLogging
import SGSimpleSettings
import SGStrings
import SGAPIToken
import SGItemListUI
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKit
import MessageUI
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AppBundle
import WebKit
import PeerNameColorScreen
import UndoUI
private enum SGControllerSection: Int32, SGItemListSection {
case search
case content
case tabs
case folders
case chatList
case profiles
case stories
case translation
case photo
case stickers
case videoNotes
case contextMenu
case accountColors
case other
}
private enum SGBoolSetting: String {
case hidePhoneInSettings
case showTabNames
case showContactsTab
case showCallsTab
case foldersAtBottom
case startTelescopeWithRearCam
case hideStories
case uploadSpeedBoost
case showProfileId
case warnOnStoriesOpen
case sendWithReturnKey
case rememberLastFolder
case sendLargePhotos
case storyStealthMode
case disableSwipeToRecordStory
case disableDeleteChatSwipeOption
case quickTranslateButton
case hideReactions
case showRepostToStory
case contextShowSelectFromUser
case contextShowSaveToCloud
case contextShowHideForwardName
case contextShowRestrict
case contextShowReport
case contextShowReply
case contextShowPin
case contextShowSaveMedia
case contextShowMessageReplies
case contextShowJson
case disableScrollToNextChannel
case disableScrollToNextTopic
case disableChatSwipeOptions
case disableGalleryCamera
case disableGalleryCameraPreview
case disableSendAsButton
case disableSnapDeletionEffect
case stickerTimestamp
case hideRecordingButton
case hideTabBar
case showDC
case showCreationDate
case showRegDate
case compactChatList
case compactFolderNames
case allChatsHidden
case defaultEmojisFirst
case messageDoubleTapActionOutgoingEdit
case wideChannelPosts
case forceEmojiTab
case forceBuiltInMic
case secondsInMessages
case hideChannelBottomButton
case confirmCalls
case swipeForVideoPIP
}
private enum SGOneFromManySetting: String {
case bottomTabStyle
case downloadSpeedBoost
case allChatsTitleLengthOverride
// case allChatsFolderPositionOverride
}
private enum SGSliderSetting: String {
case accountColorsSaturation
case outgoingPhotoQuality
case stickerSize
}
private enum SGDisclosureLink: String {
case contentSettings
case languageSettings
}
private struct PeerNameColorScreenState: Equatable {
var updatedNameColor: PeerNameColor?
var updatedBackgroundEmojiId: Int64?
}
private struct SGSettingsControllerState: Equatable {
var searchQuery: String?
}
private typealias SGControllerEntry = SGItemListUIEntry<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.showChannelBottomButton", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .wideChannelPosts, value: SGSimpleSettings.shared.wideChannelPosts, text: i18n("Settings.wideChannelPosts", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .forceBuiltInMic, value: SGSimpleSettings.shared.forceBuiltInMic, text: i18n("Settings.forceBuiltInMic", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.forceBuiltInMic.Notice", lang)))
entries.append(.toggle(id: id.count, section: .other, settingName: .secondsInMessages, value: SGSimpleSettings.shared.secondsInMessages, text: i18n("Settings.secondsInMessages", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .messageDoubleTapActionOutgoingEdit, value: SGSimpleSettings.shared.messageDoubleTapActionOutgoing == SGSimpleSettings.MessageDoubleTapAction.edit.rawValue, text: i18n("Settings.messageDoubleTapActionOutgoingEdit", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .hideRecordingButton, value: !SGSimpleSettings.shared.hideRecordingButton, text: i18n("Settings.RecordingButton", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSnapDeletionEffect, value: !SGSimpleSettings.shared.disableSnapDeletionEffect, text: i18n("Settings.SnapDeletionEffect", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSendAsButton, value: !SGSimpleSettings.shared.disableSendAsButton, text: i18n("Settings.SendAsButton", lang, presentationData.strings.Conversation_SendMesageAs), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableGalleryCamera, value: !SGSimpleSettings.shared.disableGalleryCamera, text: i18n("Settings.GalleryCamera", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableGalleryCameraPreview, value: !SGSimpleSettings.shared.disableGalleryCameraPreview, text: i18n("Settings.GalleryCameraPreview", lang), enabled: !SGSimpleSettings.shared.disableGalleryCamera))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextChannel, value: !SGSimpleSettings.shared.disableScrollToNextChannel, text: i18n("Settings.PullToNextChannel", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextTopic, value: !SGSimpleSettings.shared.disableScrollToNextTopic, text: i18n("Settings.PullToNextTopic", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .hideReactions, value: SGSimpleSettings.shared.hideReactions, text: i18n("Settings.HideReactions", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .uploadSpeedBoost, value: SGSimpleSettings.shared.uploadSpeedBoost, text: i18n("Settings.UploadsBoost", lang), enabled: true))
entries.append(.oneFromManySelector(id: id.count, section: .other, settingName: .downloadSpeedBoost, text: i18n("Settings.DownloadsBoost", lang), value: i18n("Settings.DownloadsBoost.\(SGSimpleSettings.shared.downloadSpeedBoost)", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.DownloadsBoost.Notice", lang)))
entries.append(.toggle(id: id.count, section: .other, settingName: .sendWithReturnKey, value: SGSettings.sendWithReturnKey, text: i18n("Settings.SendWithReturnKey", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .forceEmojiTab, value: SGSimpleSettings.shared.forceEmojiTab, text: i18n("Settings.ForceEmojiTab", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .other, settingName: .defaultEmojisFirst, value: SGSimpleSettings.shared.defaultEmojisFirst, text: i18n("Settings.DefaultEmojisFirst", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.DefaultEmojisFirst.Notice", lang)))
entries.append(.toggle(id: id.count, section: .other, settingName: .hidePhoneInSettings, value: SGSimpleSettings.shared.hidePhoneInSettings, text: i18n("Settings.HidePhoneInSettingsUI", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.HidePhoneInSettingsUI.Notice", lang)))
return filterSGItemListUIEntrires(entries: entries, by: state.searchQuery)
}
public func sgSettingsController(context: AccountContext/*, focusOnItemTag: Int? = nil*/) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
// var getRootControllerImpl: (() -> UIViewController?)?
// var getNavigationControllerImpl: (() -> NavigationController?)?
var askForRestart: (() -> Void)?
let initialState = SGSettingsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((SGSettingsControllerState) -> SGSettingsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
// let sliderPromise = ValuePromise(SGSimpleSettings.shared.accountColorsSaturation, ignoreRepeated: true)
// let sliderStateValue = Atomic(value: SGSimpleSettings.shared.accountColorsSaturation)
// let _: ((Int32) -> Int32) -> Void = { f in
// sliderPromise.set(sliderStateValue.modify( {f($0)}))
// }
let simplePromise = ValuePromise(true, ignoreRepeated: false)
let arguments = SGItemListArguments<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
simplePromise.set(true)
case .disableGalleryCameraPreview:
SGSimpleSettings.shared.disableGalleryCameraPreview = !value
case .disableSendAsButton:
SGSimpleSettings.shared.disableSendAsButton = !value
case .disableSnapDeletionEffect:
SGSimpleSettings.shared.disableSnapDeletionEffect = !value
case .contextShowReport:
SGSimpleSettings.shared.contextShowReport = value
case .contextShowReply:
SGSimpleSettings.shared.contextShowReply = value
case .contextShowPin:
SGSimpleSettings.shared.contextShowPin = value
case .contextShowSaveMedia:
SGSimpleSettings.shared.contextShowSaveMedia = value
case .contextShowMessageReplies:
SGSimpleSettings.shared.contextShowMessageReplies = value
case .stickerTimestamp:
SGSimpleSettings.shared.stickerTimestamp = value
case .contextShowJson:
SGSimpleSettings.shared.contextShowJson = value
case .hideRecordingButton:
SGSimpleSettings.shared.hideRecordingButton = !value
case .hideTabBar:
SGSimpleSettings.shared.hideTabBar = value
simplePromise.set(true) // Trigger update for 'enabled' field of other toggles
askForRestart?()
case .showDC:
SGSimpleSettings.shared.showDC = value
case .showCreationDate:
SGSimpleSettings.shared.showCreationDate = value
case .showRegDate:
SGSimpleSettings.shared.showRegDate = value
case .compactChatList:
SGSimpleSettings.shared.compactChatList = value
askForRestart?()
case .compactFolderNames:
SGSimpleSettings.shared.compactFolderNames = value
case .allChatsHidden:
SGSimpleSettings.shared.allChatsHidden = value
askForRestart?()
case .defaultEmojisFirst:
SGSimpleSettings.shared.defaultEmojisFirst = value
case .messageDoubleTapActionOutgoingEdit:
SGSimpleSettings.shared.messageDoubleTapActionOutgoing = value ? SGSimpleSettings.MessageDoubleTapAction.edit.rawValue : SGSimpleSettings.MessageDoubleTapAction.default.rawValue
case .wideChannelPosts:
SGSimpleSettings.shared.wideChannelPosts = value
case .forceEmojiTab:
SGSimpleSettings.shared.forceEmojiTab = value
case .forceBuiltInMic:
SGSimpleSettings.shared.forceBuiltInMic = value
case .hideChannelBottomButton:
SGSimpleSettings.shared.hideChannelBottomButton = !value
case .secondsInMessages:
SGSimpleSettings.shared.secondsInMessages = value
case .confirmCalls:
SGSimpleSettings.shared.confirmCalls = value
case .swipeForVideoPIP:
SGSimpleSettings.shared.videoPIPSwipeDirection = value ? SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue : SGSimpleSettings.VideoPIPSwipeDirection.none.rawValue
}
}, updateSliderValue: { setting, value in
switch (setting) {
case .accountColorsSaturation:
if SGSimpleSettings.shared.accountColorsSaturation != value {
SGSimpleSettings.shared.accountColorsSaturation = value
simplePromise.set(true)
}
case .outgoingPhotoQuality:
if SGSimpleSettings.shared.outgoingPhotoQuality != value {
SGSimpleSettings.shared.outgoingPhotoQuality = value
simplePromise.set(true)
}
case .stickerSize:
if SGSimpleSettings.shared.stickerSize != value {
SGSimpleSettings.shared.stickerSize = value
simplePromise.set(true)
}
}
}, setOneFromManyValue: { setting in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
switch (setting) {
case .downloadSpeedBoost:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.downloadSpeedBoost = value
let enableDownloadX: Bool
switch (value) {
case SGSimpleSettings.DownloadSpeedBoostValues.none.rawValue:
enableDownloadX = false
default:
enableDownloadX = true
}
// Updating controller
simplePromise.set(true)
let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in
var settings = settings
settings.useExperimentalDownload = enableDownloadX
return settings
}).start(completed: {
Queue.mainQueue().async {
askForRestart?()
}
})
}
for value in SGSimpleSettings.DownloadSpeedBoostValues.allCases {
items.append(ActionSheetButtonItem(title: i18n("Settings.DownloadsBoost.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
case .bottomTabStyle:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.bottomTabStyle = value
simplePromise.set(true)
}
for value in SGSimpleSettings.BottomTabStyleValues.allCases {
items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.BottomTabStyle.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
case .allChatsTitleLengthOverride:
let setAction: (String) -> Void = { value in
SGSimpleSettings.shared.allChatsTitleLengthOverride = value
simplePromise.set(true)
}
for value in SGSimpleSettings.AllChatsTitleLengthOverride.allCases {
let title: String
switch (value) {
case SGSimpleSettings.AllChatsTitleLengthOverride.short:
title = "\"\(presentationData.strings.ChatList_Tabs_All)\""
case SGSimpleSettings.AllChatsTitleLengthOverride.long:
title = "\"\(presentationData.strings.ChatList_Tabs_AllChats)\""
default:
title = i18n("Settings.Folders.AllChatsTitle.none", presentationData.strings.baseLanguageCode)
}
items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
setAction(value.rawValue)
}))
}
// case .allChatsFolderPositionOverride:
// let setAction: (String) -> Void = { value in
// SGSimpleSettings.shared.allChatsFolderPositionOverride = value
// simplePromise.set(true)
// }
//
// for value in SGSimpleSettings.AllChatsFolderPositionOverride.allCases {
// items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.AllChatsTitle.\(value)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// setAction(value.rawValue)
// }))
// }
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openDisclosureLink: { link in
switch (link) {
case .languageSettings:
pushControllerImpl?(context.sharedContext.makeLocalizationListController(context: context))
case .contentSettings:
let _ = (getSGSettingsURL(context: context) |> deliverOnMainQueue).start(next: { [weak context] url in
guard let strongContext = context else {
return
}
strongContext.sharedContext.applicationBindings.openUrl(url)
})
}
}, searchInput: { searchQuery in
updateState { state in
var updatedState = state
updatedState.searchQuery = searchQuery
return updatedState
}
})
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings])
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings, PreferencesKeys.appConfiguration])
let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network)
|> map(Optional.init)
let contentSettingsConfiguration = Promise<ContentSettingsConfiguration?>()
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,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGSimpleSettings",
module_name = "SGSimpleSettings",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGAppGroupIdentifier:SGAppGroupIdentifier",
],
visibility = [
"//visibility:public",
],
)

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