mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Pro
This commit is contained in:
parent
b22e91bdfa
commit
cdd2bafe40
@ -20,813 +20,6 @@ import OverlayStatusController
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
import FLEX
|
import FLEX
|
||||||
#endif
|
#endif
|
||||||
import Security
|
|
||||||
|
|
||||||
|
|
||||||
let BACKUP_SERVICE: String = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
|
|
||||||
|
|
||||||
enum KeychainError: Error {
|
|
||||||
case duplicateEntry
|
|
||||||
case unknown(OSStatus)
|
|
||||||
case itemNotFound
|
|
||||||
case invalidItemFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeychainBackupManager {
|
|
||||||
static let shared = KeychainBackupManager()
|
|
||||||
private let service = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
// MARK: - Save Credentials
|
|
||||||
func saveSession(id: String, _ session: Data) throws {
|
|
||||||
// Create query dictionary
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: id,
|
|
||||||
kSecValueData as String: session,
|
|
||||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
|
|
||||||
]
|
|
||||||
|
|
||||||
// Add to keychain
|
|
||||||
let status = SecItemAdd(query as CFDictionary, nil)
|
|
||||||
|
|
||||||
if status == errSecDuplicateItem {
|
|
||||||
// Item already exists, update it
|
|
||||||
let updateQuery: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: id
|
|
||||||
]
|
|
||||||
|
|
||||||
let attributesToUpdate: [String: Any] = [
|
|
||||||
kSecValueData as String: session
|
|
||||||
]
|
|
||||||
|
|
||||||
let updateStatus = SecItemUpdate(updateQuery as CFDictionary,
|
|
||||||
attributesToUpdate as CFDictionary)
|
|
||||||
|
|
||||||
if updateStatus != errSecSuccess {
|
|
||||||
throw KeychainError.unknown(updateStatus)
|
|
||||||
}
|
|
||||||
} else if status != errSecSuccess {
|
|
||||||
throw KeychainError.unknown(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Retrieve Credentials
|
|
||||||
func retrieveSession(for id: String) throws -> Data {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: id,
|
|
||||||
kSecReturnData as String: true
|
|
||||||
]
|
|
||||||
|
|
||||||
var result: AnyObject?
|
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
||||||
|
|
||||||
guard status == errSecSuccess, let sessionData = result as? Data else {
|
|
||||||
throw KeychainError.itemNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionData
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Delete Credentials
|
|
||||||
func deleteSession(for id: String) throws {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: id
|
|
||||||
]
|
|
||||||
|
|
||||||
let status = SecItemDelete(query as CFDictionary)
|
|
||||||
|
|
||||||
if status != errSecSuccess && status != errSecItemNotFound {
|
|
||||||
throw KeychainError.unknown(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Retrieve All Accounts
|
|
||||||
func getAllSessons() throws -> [Data] {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecReturnData as String: true,
|
|
||||||
kSecMatchLimit as String: kSecMatchLimitAll
|
|
||||||
]
|
|
||||||
|
|
||||||
var result: AnyObject?
|
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
||||||
|
|
||||||
if status == errSecItemNotFound {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
guard status == errSecSuccess,
|
|
||||||
let credentialsDataArray = result as? [Data] else {
|
|
||||||
throw KeychainError.unknown(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentialsDataArray
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Delete All Sessions
|
|
||||||
func deleteAllSessions() throws {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service
|
|
||||||
]
|
|
||||||
|
|
||||||
let status = SecItemDelete(query as CFDictionary)
|
|
||||||
|
|
||||||
// If no items were found, that's fine - just return
|
|
||||||
if status == errSecItemNotFound {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For any other error, throw
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError.unknown(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SessionBackup: Codable {
|
|
||||||
var name: String? = nil
|
|
||||||
var date: Date = Date()
|
|
||||||
let accountRecord: AccountRecord<TelegramAccountManagerTypes.Attribute>
|
|
||||||
|
|
||||||
var peerIdInternal: Int64 {
|
|
||||||
var userId: Int64 = 0
|
|
||||||
for attribute in accountRecord.attributes {
|
|
||||||
if case let .backupData(backupData) = attribute, let backupPeerID = backupData.data?.peerId {
|
|
||||||
userId = backupPeerID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userId
|
|
||||||
}
|
|
||||||
|
|
||||||
var userId: Int64 {
|
|
||||||
return PeerId(peerIdInternal).id._internalGetInt64Value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SGSwiftUI
|
|
||||||
import LegacyUI
|
|
||||||
import SGStrings
|
|
||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct SessionBackupRow: View {
|
|
||||||
let backup: SessionBackup
|
|
||||||
let isLoggedIn: Bool
|
|
||||||
|
|
||||||
|
|
||||||
private let dateFormatter: DateFormatter = {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateStyle = .short
|
|
||||||
formatter.timeStyle = .short
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
|
|
||||||
var formattedDate: String {
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
return backup.date.formatted(date: .abbreviated, time: .shortened)
|
|
||||||
} else {
|
|
||||||
return dateFormatter.string(from: backup.date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(backup.name ?? String(backup.userId))
|
|
||||||
.font(.body)
|
|
||||||
|
|
||||||
Text("ID: \(backup.userId)")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Last Backup: \(formattedDate)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(isLoggedIn ? "Logged In" : "Logged Out")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.vertical, 2)
|
|
||||||
.background(Color.secondary.opacity(0.1))
|
|
||||||
.cornerRadius(4)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct BorderedButtonStyle: ButtonStyle {
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
|
||||||
configuration.label
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.accentColor, lineWidth: 1)
|
|
||||||
)
|
|
||||||
.opacity(configuration.isPressed ? 0.7 : 1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct SessionBackupManagerView: View {
|
|
||||||
weak var wrapperController: LegacyController?
|
|
||||||
let context: AccountContext
|
|
||||||
|
|
||||||
@State private var sessions: [SessionBackup] = []
|
|
||||||
@State private var loggedInPeerIDs: [Int64] = []
|
|
||||||
@State private var loggedInAccountsDisposable: Disposable? = nil
|
|
||||||
|
|
||||||
private func performBackup() {
|
|
||||||
let controller = OverlayStatusController(theme: context.sharedContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil))
|
|
||||||
|
|
||||||
let signal = context.sharedContext.accountManager.accountRecords()
|
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
let signal2 = context.sharedContext.activeAccountsWithInfo
|
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
|
|
||||||
Task {
|
|
||||||
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("OK: \(restoredSessions) Sessions restored", presentationData),
|
|
||||||
in: .current
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let backup = backupSessions[index]
|
|
||||||
|
|
||||||
// Check for existing record
|
|
||||||
let existingRecord = view.records.first { record in
|
|
||||||
var userId: Int64 = 0
|
|
||||||
for attribute in record.attributes {
|
|
||||||
if case let .backupData(backupData) = attribute {
|
|
||||||
userId = backupData.data?.peerId ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userId == backup.peerIdInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingRecord != nil {
|
|
||||||
print("Record \(backup.userId) already exists, skipping")
|
|
||||||
importNextBackup(index: index + 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var importAttributes = backup.accountRecord.attributes
|
|
||||||
importAttributes.removeAll { attribute in
|
|
||||||
if case .sortOrder = attribute {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let importBackupSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
|
|
||||||
let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in
|
|
||||||
for attribute in record.attributes {
|
|
||||||
if case let .sortOrder(sortOrder) = attribute {
|
|
||||||
return sortOrder.order
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}).max() ?? 0) + 1
|
|
||||||
importAttributes.append(.sortOrder(AccountSortOrderAttribute(order: nextSortOrder)))
|
|
||||||
let accountRecordId = transaction.createRecord(importAttributes)
|
|
||||||
print("Imported record \(accountRecordId) for \(backup.userId)")
|
|
||||||
restoredSessions += 1
|
|
||||||
}
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
let _ = importBackupSignal.start(completed: {
|
|
||||||
importNextBackup(index: index + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the import chain
|
|
||||||
importNextBackup(index: 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performDeleteAll() {
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
|
|
||||||
let controller = textAlertController(context: context, title: "Delete All Backups?", text: "All sessions will be removed from Keychain.\n\nAccounts will not be logged out from Swiftgram.", actions: [
|
|
||||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
do {
|
|
||||||
try KeychainBackupManager.shared.deleteAllSessions()
|
|
||||||
withAnimation {
|
|
||||||
sessions = getBackedSessions()
|
|
||||||
}
|
|
||||||
controller.dismiss()
|
|
||||||
} catch let e {
|
|
||||||
print("Error deleting all sessions: \(e)")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
|
||||||
])
|
|
||||||
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performDelete(_ session: SessionBackup) {
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
|
|
||||||
let controller = textAlertController(context: context, title: "Delete 1 Backup?", text: "\(session.name ?? "\(session.userId)") session will be removed from Keychain.\n\nAccount will not be logged out from Swiftgram.", actions: [
|
|
||||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
do {
|
|
||||||
try KeychainBackupManager.shared.deleteSession(for: "\(session.peerIdInternal)")
|
|
||||||
withAnimation {
|
|
||||||
sessions = getBackedSessions()
|
|
||||||
}
|
|
||||||
controller.dismiss()
|
|
||||||
} catch let e {
|
|
||||||
print("Error deleting session: \(e)")
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
|
||||||
])
|
|
||||||
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private func performRemoveSessionFromApp(session: SessionBackup) {
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
|
|
||||||
let controller = textAlertController(context: context, title: "Remove session from App?", text: "\(session.name ?? "\(session.userId)") session will be removed from app? Account WILL BE logged out of Swiftgram.", actions: [
|
|
||||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
|
|
||||||
let signal = context.sharedContext.accountManager.accountRecords()
|
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
let _ = signal.start(next: { [weak controller] view in
|
|
||||||
|
|
||||||
// Find record to delete
|
|
||||||
let accountRecord = view.records.first { record in
|
|
||||||
var userId: Int64 = 0
|
|
||||||
for attribute in record.attributes {
|
|
||||||
if case let .backupData(backupData) = attribute {
|
|
||||||
userId = backupData.data?.peerId ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userId == session.peerIdInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
if let record = accountRecord {
|
|
||||||
let deleteSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
|
|
||||||
transaction.updateRecord(record.id, { _ in return nil})
|
|
||||||
}
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
let _ = deleteSignal.start(next: {
|
|
||||||
withAnimation {
|
|
||||||
sessions = getBackedSessions()
|
|
||||||
}
|
|
||||||
controller?.dismiss()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
controller?.dismiss()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}),
|
|
||||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
|
||||||
])
|
|
||||||
|
|
||||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
Section(header: Text("Actions")) {
|
|
||||||
Button(action: performBackup) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "key.fill")
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Backup to Keychain")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: performRestore) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "arrow.2.circlepath")
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Restore from Keychain")
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: performDeleteAll) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "trash")
|
|
||||||
.frame(width: 30)
|
|
||||||
Text("Delete Keychain Backup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundColor(.red)
|
|
||||||
// Text("Removing sessions from Keychain. This will not affect logged-in accounts.")
|
|
||||||
// .font(.caption)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Backups")) {
|
|
||||||
ForEach(sessions, id: \.peerIdInternal) { session in
|
|
||||||
SessionBackupRow(
|
|
||||||
backup: session,
|
|
||||||
isLoggedIn: loggedInPeerIDs.contains(session.peerIdInternal)
|
|
||||||
)
|
|
||||||
.contextMenu {
|
|
||||||
Button(action: {
|
|
||||||
performDelete(session)
|
|
||||||
}, label: {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text("Delete from Backup")
|
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
#if DEBUG
|
|
||||||
Button(action: {
|
|
||||||
performRemoveSessionFromApp(session: session)
|
|
||||||
}, label: {
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text("Remove from App")
|
|
||||||
Image(systemName: "trash")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// .onDelete { indexSet in
|
|
||||||
// performDelete(indexSet)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
withAnimation {
|
|
||||||
sessions = getBackedSessions()
|
|
||||||
}
|
|
||||||
|
|
||||||
let accountsSignal = context.sharedContext.accountManager.accountRecords()
|
|
||||||
|> deliverOnMainQueue
|
|
||||||
|
|
||||||
loggedInAccountsDisposable = accountsSignal.start(next: { view in
|
|
||||||
var result: [Int64] = []
|
|
||||||
for record in view.records {
|
|
||||||
var isLoggedOut: Bool = false
|
|
||||||
var userId: Int64 = 0
|
|
||||||
for attribute in record.attributes {
|
|
||||||
if case .loggedOut = attribute {
|
|
||||||
isLoggedOut = true
|
|
||||||
} else if case let .backupData(backupData) = attribute {
|
|
||||||
userId = backupData.data?.peerId ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isLoggedOut && userId != 0 {
|
|
||||||
result.append(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Will check logged in accounts")
|
|
||||||
if loggedInPeerIDs != result {
|
|
||||||
print("Updating logged in accounts", result)
|
|
||||||
loggedInPeerIDs = result
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
loggedInAccountsDisposable?.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func getBackedSessions() -> [SessionBackup] {
|
|
||||||
var sessions: [SessionBackup] = []
|
|
||||||
do {
|
|
||||||
let backupSessionsData = try KeychainBackupManager.shared.getAllSessons()
|
|
||||||
for sessionBackupData in backupSessionsData {
|
|
||||||
do {
|
|
||||||
let backup = try JSONDecoder().decode(SessionBackup.self, from: sessionBackupData)
|
|
||||||
sessions.append(backup)
|
|
||||||
} catch let e {
|
|
||||||
print("IMPORT ERROR: \(e)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch let e {
|
|
||||||
print("Error getting all sessions: \(e)")
|
|
||||||
}
|
|
||||||
return sessions
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func backupSessionsFromView(_ view: AccountRecordsView<TelegramAccountManagerTypes>, accountsWithInfo: [AccountWithInfo] = []) {
|
|
||||||
var recordsToBackup: [Int64: AccountRecord<TelegramAccountManagerTypes.Attribute>] = [:]
|
|
||||||
for record in view.records {
|
|
||||||
var sortOrder: Int32 = 0
|
|
||||||
var isLoggedOut: Bool = false
|
|
||||||
var isTestingEnvironment: Bool = false
|
|
||||||
var peerId: Int64 = 0
|
|
||||||
for attribute in record.attributes {
|
|
||||||
if case let .sortOrder(value) = attribute {
|
|
||||||
sortOrder = value.order
|
|
||||||
} else if case .loggedOut = attribute {
|
|
||||||
isLoggedOut = true
|
|
||||||
} else if case let .environment(environment) = attribute, case .test = environment.environment {
|
|
||||||
isTestingEnvironment = true
|
|
||||||
} else if case let .backupData(backupData) = attribute {
|
|
||||||
peerId = backupData.data?.peerId ?? 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = sortOrder
|
|
||||||
let _ = isTestingEnvironment
|
|
||||||
|
|
||||||
if !isLoggedOut && peerId != 0 {
|
|
||||||
recordsToBackup[peerId] = record
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (peerId, record) in recordsToBackup {
|
|
||||||
var backupName: String? = nil
|
|
||||||
if let accountWithInfo = accountsWithInfo.first(where: { $0.peer.id == PeerId(peerId) }) {
|
|
||||||
if let user = accountWithInfo.peer as? TelegramUser {
|
|
||||||
if let username = user.username {
|
|
||||||
backupName = "@\(username)"
|
|
||||||
} else {
|
|
||||||
backupName = user.nameOrPhone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let backup = SessionBackup(name: backupName, accountRecord: record)
|
|
||||||
do {
|
|
||||||
let data = try JSONEncoder().encode(backup)
|
|
||||||
try KeychainBackupManager.shared.saveSession(id: "\(backup.peerIdInternal)", data)
|
|
||||||
} catch let e {
|
|
||||||
print("BACKUP ERROR: \(e)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
public func sgSessionBackupManagerController(context: AccountContext, presentationData: PresentationData? = nil) -> ViewController {
|
|
||||||
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
|
||||||
let strings = presentationData?.strings ?? defaultPresentationStrings
|
|
||||||
|
|
||||||
let legacyController = LegacySwiftUIController(
|
|
||||||
presentation: .navigation,
|
|
||||||
theme: theme,
|
|
||||||
strings: strings
|
|
||||||
)
|
|
||||||
legacyController.statusBar.statusBarStyle = theme.rootController
|
|
||||||
.statusBarStyle.style
|
|
||||||
legacyController.title = "Session Backup" //i18n("BackupManager.Title", strings.baseLanguageCode)
|
|
||||||
|
|
||||||
let swiftUIView = SGSwiftUIView<SessionBackupManagerView>(
|
|
||||||
legacyController: legacyController,
|
|
||||||
manageSafeArea: true,
|
|
||||||
content: {
|
|
||||||
SessionBackupManagerView(wrapperController: legacyController, context: context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
|
||||||
legacyController.bind(controller: controller)
|
|
||||||
|
|
||||||
return legacyController
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct MessageFilterKeywordInputFieldModifier: ViewModifier {
|
|
||||||
@Binding var newKeyword: String
|
|
||||||
let onAdd: () -> Void
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
content
|
|
||||||
.submitLabel(.return)
|
|
||||||
.submitScope(false) // TODO(swiftgram): Keyboard still closing
|
|
||||||
.interactiveDismissDisabled()
|
|
||||||
.onSubmit {
|
|
||||||
onAdd()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct MessageFilterKeywordInputView: View {
|
|
||||||
@Binding var newKeyword: String
|
|
||||||
let onAdd: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
TextField("Enter keyword", text: $newKeyword)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.autocapitalization(.none)
|
|
||||||
.keyboardType(.default)
|
|
||||||
.modifier(MessageFilterKeywordInputFieldModifier(newKeyword: $newKeyword, onAdd: onAdd))
|
|
||||||
|
|
||||||
|
|
||||||
Button(action: onAdd) {
|
|
||||||
Image(systemName: "plus.circle.fill")
|
|
||||||
.foregroundColor(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor)
|
|
||||||
.imageScale(.large)
|
|
||||||
}
|
|
||||||
.disabled(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
struct MessageFilterView: View {
|
|
||||||
weak var wrapperController: LegacyController?
|
|
||||||
|
|
||||||
@State private var newKeyword: String = ""
|
|
||||||
@State private var keywords: [String] {
|
|
||||||
didSet {
|
|
||||||
SGSimpleSettings.shared.messageFilterKeywords = keywords
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(wrapperController: LegacyController?) {
|
|
||||||
self.wrapperController = wrapperController
|
|
||||||
_keywords = State(initialValue: SGSimpleSettings.shared.messageFilterKeywords)
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyContent: some View {
|
|
||||||
List {
|
|
||||||
Section {
|
|
||||||
// Icon and title
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: "nosign.app.fill")
|
|
||||||
.font(.system(size: 50))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Message Filter")
|
|
||||||
.font(.title)
|
|
||||||
.bold()
|
|
||||||
|
|
||||||
Text("Remove distraction and reduce visibility of messages containing keywords below.\nKeywords are case-sensitive.")
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
.listRowInsets(EdgeInsets())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
MessageFilterKeywordInputView(newKeyword: $newKeyword, onAdd: addKeyword)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Keywords")) {
|
|
||||||
ForEach(keywords.reversed(), id: \.self) { keyword in
|
|
||||||
Text(keyword)
|
|
||||||
}
|
|
||||||
.onDelete { indexSet in
|
|
||||||
let originalIndices = IndexSet(
|
|
||||||
indexSet.map { keywords.count - 1 - $0 }
|
|
||||||
)
|
|
||||||
deleteKeywords(at: originalIndices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tgNavigationBackButton(wrapperController: wrapperController)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
bodyContent
|
|
||||||
.toolbar {
|
|
||||||
EditButton()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bodyContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addKeyword() {
|
|
||||||
let trimmedKeyword = newKeyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmedKeyword.isEmpty else { return }
|
|
||||||
|
|
||||||
let keywordExists = keywords.contains {
|
|
||||||
$0 == trimmedKeyword
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !keywordExists else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
withAnimation {
|
|
||||||
keywords.append(trimmedKeyword)
|
|
||||||
}
|
|
||||||
newKeyword = ""
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deleteKeywords(at offsets: IndexSet) {
|
|
||||||
withAnimation {
|
|
||||||
keywords.remove(atOffsets: offsets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
|
||||||
public func sgMessageFilterController(presentationData: PresentationData? = nil) -> ViewController {
|
|
||||||
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
|
||||||
let strings = presentationData?.strings ?? defaultPresentationStrings
|
|
||||||
|
|
||||||
let legacyController = LegacySwiftUIController(
|
|
||||||
presentation: .navigation,
|
|
||||||
theme: theme,
|
|
||||||
strings: strings
|
|
||||||
)
|
|
||||||
// Status bar color will break if theme changed
|
|
||||||
legacyController.statusBar.statusBarStyle = theme.rootController
|
|
||||||
.statusBarStyle.style
|
|
||||||
legacyController.displayNavigationBar = false
|
|
||||||
let swiftUIView = MessageFilterView(wrapperController: legacyController)
|
|
||||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
|
||||||
legacyController.bind(controller: controller)
|
|
||||||
|
|
||||||
return legacyController
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private enum SGDebugControllerSection: Int32, SGItemListSection {
|
private enum SGDebugControllerSection: Int32, SGItemListSection {
|
||||||
@ -868,25 +61,13 @@ private func SGDebugControllerEntries(presentationData: PresentationData) -> [SG
|
|||||||
#if DEBUG
|
#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: .flexing, text: "FLEX", kind: .generic))
|
||||||
entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", kind: .generic))
|
entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", kind: .generic))
|
||||||
entries.append(.disclosure(id: id.count, section: .base, link: .debugIAP, text: "Pro"))
|
|
||||||
entries.append(.action(id: id.count, section: .base, actionType: .resetIAP, text: "Reset Pro", kind: .destructive))
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if SGSimpleSettings.shared.b {
|
|
||||||
entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "Session Backup"))
|
|
||||||
entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "Message Filter"))
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
entries.append(.toggle(id: id.count, section: .base, settingName: .inputToolbar, value: SGSimpleSettings.shared.inputToolbar, text: "Message Formatting Toolbar", enabled: true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
|
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
|
||||||
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
|
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
|
||||||
|
entries.append(.action(id: id.count, section: .base, actionType: .resetIAP, text: "Reset Pro", kind: .destructive))
|
||||||
|
|
||||||
entries.append(.header(id: id.count, section: .notifications, text: "NOTIFICATIONS", badge: nil))
|
entries.append(.toggle(id: id.count, section: .notifications, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[OLD] Fix empty notifications", enabled: true))
|
||||||
entries.append(.toggle(id: id.count, section: .notifications, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[Legacy] Fix empty notifications", enabled: true))
|
|
||||||
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .pinnedMessageNotifications, text: "Pinned Messages", value: SGSimpleSettings.shared.pinnedMessageNotifications, enabled: true))
|
|
||||||
entries.append(.oneFromManySelector(id: id.count, section: .notifications, settingName: .mentionsAndRepliesNotifications, text: "@Mentions and Replies", value: SGSimpleSettings.shared.mentionsAndRepliesNotifications, enabled: true))
|
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
private func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController {
|
private func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController {
|
||||||
@ -913,44 +94,11 @@ public func sgDebugController(context: AccountContext) -> ViewController {
|
|||||||
}, setOneFromManyValue: { setting in
|
}, setOneFromManyValue: { setting in
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||||
var items: [ActionSheetItem] = []
|
let items: [ActionSheetItem] = []
|
||||||
|
// var items: [ActionSheetItem] = []
|
||||||
|
|
||||||
switch (setting) {
|
// 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: value.rawValue, color: .accent, action: { [weak actionSheet] in
|
|
||||||
actionSheet?.dismissAnimated()
|
|
||||||
if SGSimpleSettings.shared.b {
|
|
||||||
setAction(value.rawValue)
|
|
||||||
} else {
|
|
||||||
setAction(SGSimpleSettings.PinnedMessageNotificationsSettings.default.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: value.rawValue, color: .accent, action: { [weak actionSheet] in
|
|
||||||
actionSheet?.dismissAnimated()
|
|
||||||
if SGSimpleSettings.shared.b {
|
|
||||||
setAction(value.rawValue)
|
|
||||||
} else {
|
|
||||||
setAction(SGSimpleSettings.MentionsAndRepliesNotificationsSettings.default.rawValue)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||||
@ -958,53 +106,7 @@ public func sgDebugController(context: AccountContext) -> ViewController {
|
|||||||
})
|
})
|
||||||
])])
|
])])
|
||||||
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||||
}, openDisclosureLink: { link in
|
}, openDisclosureLink: { _ in
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
switch (link) {
|
|
||||||
case .sessionBackupManager:
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
|
|
||||||
} else {
|
|
||||||
presentControllerImpl?(UndoOverlayController(
|
|
||||||
presentationData: presentationData,
|
|
||||||
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
|
|
||||||
elevatedLayout: false,
|
|
||||||
action: { _ in return false }
|
|
||||||
), nil)
|
|
||||||
}
|
|
||||||
case .messageFilter:
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
|
|
||||||
} else {
|
|
||||||
presentControllerImpl?(UndoOverlayController(
|
|
||||||
presentationData: presentationData,
|
|
||||||
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
|
|
||||||
elevatedLayout: false,
|
|
||||||
action: { _ in return false }
|
|
||||||
), nil)
|
|
||||||
}
|
|
||||||
case .debugIAP:
|
|
||||||
#if DEBUG
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
if let sgIAPManager = context.sharedContext.SGIAP {
|
|
||||||
let statusSignal = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus])
|
|
||||||
|> map { sharedData -> Int64 in
|
|
||||||
let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default
|
|
||||||
return sgStatus.status
|
|
||||||
}
|
|
||||||
presentControllerImpl?(sgPayWallController(statusSignal: statusSignal, replacementController: sgDebugController(context: context), presentationData: presentationData, SGIAPManager: sgIAPManager), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
presentControllerImpl?(UndoOverlayController(
|
|
||||||
presentationData: presentationData,
|
|
||||||
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
|
|
||||||
elevatedLayout: false,
|
|
||||||
action: { _ in return false }
|
|
||||||
), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}, action: { actionType in
|
}, action: { actionType in
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
switch actionType {
|
switch actionType {
|
||||||
|
17
Swiftgram/SGKeychainBackupManager/BUILD
Normal file
17
Swiftgram/SGKeychainBackupManager/BUILD
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import TelegramUIPreferences
|
|||||||
|
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
@available(iOS 13.0, *)
|
||||||
public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager) -> ViewController {
|
public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacementController: ViewController, presentationData: PresentationData? = nil, SGIAPManager: SGIAPManager, openUrl: @escaping (String) -> Void) -> ViewController {
|
||||||
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
// let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
||||||
let theme = defaultDarkColorPresentationTheme
|
let theme = defaultDarkColorPresentationTheme
|
||||||
let strings = presentationData?.strings ?? defaultPresentationStrings
|
let strings = presentationData?.strings ?? defaultPresentationStrings
|
||||||
@ -30,7 +30,7 @@ public func sgPayWallController(statusSignal: Signal<Int64, NoError>, replacemen
|
|||||||
let swiftUIView = SGSwiftUIView<SGPayWallView>(
|
let swiftUIView = SGSwiftUIView<SGPayWallView>(
|
||||||
legacyController: legacyController,
|
legacyController: legacyController,
|
||||||
content: {
|
content: {
|
||||||
SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal)
|
SGPayWallView(wrapperController: legacyController, replacementController: replacementController, SGIAP: SGIAPManager, statusSignal: statusSignal, openUrl: openUrl)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
||||||
@ -105,6 +105,7 @@ struct SGPayWallView: View {
|
|||||||
let replacementController: ViewController
|
let replacementController: ViewController
|
||||||
let SGIAP: SGIAPManager
|
let SGIAP: SGIAPManager
|
||||||
let statusSignal: Signal<Int64, NoError>
|
let statusSignal: Signal<Int64, NoError>
|
||||||
|
let openUrl: (String) -> Void
|
||||||
|
|
||||||
private enum PayWallState: Equatable {
|
private enum PayWallState: Equatable {
|
||||||
case ready // ready to buy
|
case ready // ready to buy
|
||||||
@ -147,7 +148,7 @@ struct SGPayWallView: View {
|
|||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text("Supercharged with Pro features".i18n(lang))
|
Text("PayWall.Text".i18n(lang))
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
@ -156,14 +157,18 @@ struct SGPayWallView: View {
|
|||||||
// Features
|
// Features
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
featuresSection
|
featuresSection
|
||||||
|
legalSection
|
||||||
restorePurchasesButton
|
restorePurchasesButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Spacer for purchase buttons
|
// Spacer for purchase buttons
|
||||||
Color.clear.frame(height: 50)
|
Color.clear.frame(height: 50)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 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
|
// Fixed purchase button at bottom
|
||||||
purchaseSection
|
purchaseSection
|
||||||
@ -220,7 +225,7 @@ struct SGPayWallView: View {
|
|||||||
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String, !error.isEmpty {
|
if let userInfo = notification.userInfo, let error = userInfo["error"] as? String, !error.isEmpty {
|
||||||
showErrorAlert(error)
|
showErrorAlert(error)
|
||||||
} else {
|
} else {
|
||||||
showErrorAlert("Validation Error")
|
showErrorAlert("PayWall.ValidationError".i18n(lang))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,35 +235,33 @@ struct SGPayWallView: View {
|
|||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue),
|
icon: FeatureIcon(icon: "lock.fill", backgroundColor: .blue),
|
||||||
title: "Session Backup",
|
title: "PayWall.SessionBackup.Title".i18n(lang),
|
||||||
subtitle: "Restore sessions from encrypted local Apple Keychain backup."
|
subtitle: "PayWall.SessionBackup.Notice".i18n(lang)
|
||||||
)
|
)
|
||||||
|
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold),
|
icon: FeatureIcon(icon: "nosign", backgroundColor: .gray, fontWeight: .bold),
|
||||||
title: "Message Filter",
|
title: "PayWall.MessageFilter.Title".i18n(lang),
|
||||||
subtitle: "Reduce visibility of spam, promotions and annoying messages."
|
subtitle: "PayWall.MessageFilter.Notice".i18n(lang)
|
||||||
)
|
)
|
||||||
|
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
|
icon: FeatureIcon(icon: "bell.badge.slash.fill", backgroundColor: .red),
|
||||||
title: "Disable @mentions and replies",
|
title: "PayWall.Notifications.Title".i18n(lang),
|
||||||
subtitle: "Hide or silence non-important notifications."
|
subtitle: "PayWall.MessageFilter.Notice".i18n(lang)
|
||||||
)
|
)
|
||||||
|
|
||||||
FeatureRow(
|
FeatureRow(
|
||||||
icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
|
icon: FeatureIcon(icon: "bold.underline", backgroundColor: .blue, iconSize: 16),
|
||||||
title: "Quick Formatting panel",
|
title: "PayWall.InputToolbar.Title".i18n(lang),
|
||||||
subtitle: "Save time preparing your posts with a panel right above your keyboard."
|
subtitle: "PayWall.InputToolbar.Notice".i18n(lang)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.leading, max(innerShadowWidth + 8.0, sgLeftSafeAreaInset(containerViewLayout)))
|
|
||||||
.padding(.trailing, max(innerShadowWidth + 8.0, sgRightSafeAreaInset(containerViewLayout)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var restorePurchasesButton: some View {
|
private var restorePurchasesButton: some View {
|
||||||
Button(action: handleRestorePurchases) {
|
Button(action: handleRestorePurchases) {
|
||||||
Text("Restore Purchases")
|
Text("PayWall.RestorePurchases".i18n(lang))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(Color(hex: accentColorHex))
|
.foregroundColor(Color(hex: accentColorHex))
|
||||||
@ -290,6 +293,41 @@ struct SGPayWallView: View {
|
|||||||
.shadow(radius: 8, y: -4)
|
.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 closeButtonView: some View {
|
private var closeButtonView: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
wrapperController?.dismiss(animated: true)
|
wrapperController?.dismiss(animated: true)
|
||||||
@ -306,22 +344,22 @@ struct SGPayWallView: View {
|
|||||||
|
|
||||||
private var buttonTitle: String {
|
private var buttonTitle: String {
|
||||||
if currentStatus > 1 {
|
if currentStatus > 1 {
|
||||||
return "Use Pro features".i18n(lang)
|
return "PayWall.Button.OpenPro".i18n(lang)
|
||||||
} else {
|
} else {
|
||||||
if state == .purchasing {
|
if state == .purchasing {
|
||||||
return "Purchasing...".i18n(lang)
|
return "PayWall.Button.Purchasing".i18n(lang)
|
||||||
} else if state == .restoring {
|
} else if state == .restoring {
|
||||||
return "Restoring Purchases...".i18n(lang)
|
return "PayWall.Button.Restoring".i18n(lang)
|
||||||
} else if state == .validating {
|
} else if state == .validating {
|
||||||
return "Validating Purchase...".i18n(lang)
|
return "PayWall.Button.Validating".i18n(lang)
|
||||||
} else if let product = product {
|
} else if let product = product {
|
||||||
if !SGIAP.canMakePayments {
|
if !SGIAP.canMakePayments {
|
||||||
return "Payments unavailable".i18n(lang)
|
return "PayWall.Button.PaymentsUnavailable".i18n(lang)
|
||||||
} else {
|
} else {
|
||||||
return "Subscribe for \(product.price) / month".i18n(lang, args: product.price)
|
return "PayWall.Button.Subscribe".i18n(lang, args: product.price)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "Contacting App Store...".i18n(lang)
|
return "Paywall.Button.ContactingAppStore".i18n(lang)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
Swiftgram/SGProUI/BUILD
Normal file
41
Swiftgram/SGProUI/BUILD
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
181
Swiftgram/SGProUI/Sources/MessageFilterController.swift
Normal file
181
Swiftgram/SGProUI/Sources/MessageFilterController.swift
Normal 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
|
||||||
|
}
|
159
Swiftgram/SGProUI/Sources/SGProUI.swift
Normal file
159
Swiftgram/SGProUI/Sources/SGProUI.swift
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SGProDisclosureLink: String {
|
||||||
|
case sessionBackupManager
|
||||||
|
case messageFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SGProToggles: String {
|
||||||
|
case inputToolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SGProOneFromManySetting: String {
|
||||||
|
case pinnedMessageNotifications
|
||||||
|
case mentionsAndRepliesNotifications
|
||||||
|
}
|
||||||
|
|
||||||
|
private typealias SGProControllerEntry = SGItemListUIEntry<SGProControllerSection, SGProToggles, AnyHashable, SGProOneFromManySetting, SGProDisclosureLink, AnyHashable>
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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, AnyHashable>(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: { _ in
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
520
Swiftgram/SGProUI/Sources/SessionBackupController.swift
Normal file
520
Swiftgram/SGProUI/Sources/SessionBackupController.swift
Normal 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
|
||||||
|
}
|
13
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json
vendored
Normal file
13
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "SwiftgramPro.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
BIN
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/SwiftgramPro.pdf
vendored
Normal file
BIN
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramPro.imageset/SwiftgramPro.pdf
vendored
Normal file
Binary file not shown.
@ -127,6 +127,7 @@ public class SGSimpleSettings {
|
|||||||
case pinnedMessageNotifications
|
case pinnedMessageNotifications
|
||||||
case mentionsAndRepliesNotifications
|
case mentionsAndRepliesNotifications
|
||||||
case primaryUserId
|
case primaryUserId
|
||||||
|
case status
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DownloadSpeedBoostValues: String, CaseIterable {
|
public enum DownloadSpeedBoostValues: String, CaseIterable {
|
||||||
@ -244,7 +245,8 @@ public class SGSimpleSettings {
|
|||||||
public static let groupDefaultValues: [String: Any] = [
|
public static let groupDefaultValues: [String: Any] = [
|
||||||
Keys.legacyNotificationsFix.rawValue: false,
|
Keys.legacyNotificationsFix.rawValue: false,
|
||||||
Keys.pinnedMessageNotifications.rawValue: PinnedMessageNotificationsSettings.default.rawValue,
|
Keys.pinnedMessageNotifications.rawValue: PinnedMessageNotificationsSettings.default.rawValue,
|
||||||
Keys.mentionsAndRepliesNotifications.rawValue: MentionsAndRepliesNotificationsSettings.default.rawValue
|
Keys.mentionsAndRepliesNotifications.rawValue: MentionsAndRepliesNotificationsSettings.default.rawValue,
|
||||||
|
Keys.status.rawValue: 1
|
||||||
]
|
]
|
||||||
|
|
||||||
@UserDefault(key: Keys.hidePhoneInSettings.rawValue)
|
@UserDefault(key: Keys.hidePhoneInSettings.rawValue)
|
||||||
@ -426,10 +428,10 @@ public class SGSimpleSettings {
|
|||||||
@UserDefault(key: Keys.legacyNotificationsFix.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard)
|
@UserDefault(key: Keys.legacyNotificationsFix.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard)
|
||||||
public var legacyNotificationsFix: Bool
|
public var legacyNotificationsFix: Bool
|
||||||
|
|
||||||
@UserDefault(key: Keys.legacyNotificationsFix.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard)
|
@UserDefault(key: Keys.status.rawValue, userDefaults: UserDefaults(suiteName: APP_GROUP_IDENTIFIER) ?? .standard)
|
||||||
public var status: Int64
|
public var status: Int64
|
||||||
|
|
||||||
public var b: Bool = true
|
public var ephemeralStatus: Int64 = 1
|
||||||
|
|
||||||
@UserDefault(key: Keys.messageFilterKeywords.rawValue)
|
@UserDefault(key: Keys.messageFilterKeywords.rawValue)
|
||||||
public var messageFilterKeywords: [String]
|
public var messageFilterKeywords: [String]
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
"Common.RestartNow" = "Restart Now";
|
"Common.RestartNow" = "Restart Now";
|
||||||
"Common.OpenTelegram" = "Open Telegram";
|
"Common.OpenTelegram" = "Open Telegram";
|
||||||
"Common.UseTelegramForPremium" = "Please note that to get Telegram Premium, you must use the official Telegram app. Once you have obtained Telegram Premium, all its features will become available in Swiftgram.";
|
"Common.UseTelegramForPremium" = "Please note that to get Telegram Premium, you must use the official Telegram app. Once you have obtained Telegram Premium, all its features will become available in Swiftgram.";
|
||||||
|
"Common.UpdateOS" = "iOS update required";
|
||||||
|
|
||||||
"Message.HoldToShowOrReport" = "Hold to Show or Report.";
|
"Message.HoldToShowOrReport" = "Hold to Show or Report.";
|
||||||
|
|
||||||
@ -151,3 +152,73 @@
|
|||||||
|
|
||||||
"Settings.swipeForVideoPIP" = "Video PIP with Swipe";
|
"Settings.swipeForVideoPIP" = "Video PIP with Swipe";
|
||||||
"Settings.swipeForVideoPIP.Notice" = "If enabled, swiping video will open it in Picture-in-Picture mode.";
|
"Settings.swipeForVideoPIP.Notice" = "If enabled, swiping video will open it in Picture-in-Picture mode.";
|
||||||
|
|
||||||
|
"SessionBackup.Title" = "Session Backup";
|
||||||
|
"SessionBackup.Sessions.Title" = "Sessions";
|
||||||
|
"SessionBackup.Actions.Backup" = "Backup to Keychain";
|
||||||
|
"SessionBackup.Actions.Restore" = "Restore from Keychain";
|
||||||
|
"SessionBackup.Actions.DeleteAll" = "Delete Keychain Backup";
|
||||||
|
"SessionBackup.Actions.DeleteOne" = "Delete from Backup";
|
||||||
|
"SessionBackup.Actions.RemoveFromApp" = "Remove from App";
|
||||||
|
"SessionBackup.LastBackupAt" = "Last Backup: %@";
|
||||||
|
"SessionBackup.RestoreOK" = "OK. Sessions restored: %@";
|
||||||
|
"SessionBackup.LoggedIn" = "Logged In";
|
||||||
|
"SessionBackup.LoggedOut" = "Logged Out";
|
||||||
|
"SessionBackup.DeleteAll.Title" = "Delete All Sessions?";
|
||||||
|
"SessionBackup.DeleteAll.Text" = "All sessions will be removed from Keychain.\n\nAccounts will not be logged out from Swiftgram.";
|
||||||
|
"SessionBackup.DeleteSingle.Title" = "Delete 1 (one) Session?";
|
||||||
|
"SessionBackup.DeleteSingle.Text" = "%@ session will be removed from Keychain.\n\nAccount will not be logged out from Swiftgram.";
|
||||||
|
"SessionBackup.RemoveFromApp.Title" = "Remove account from App?";
|
||||||
|
"SessionBackup.RemoveFromApp.Text" = "%@ session WILL BE REMOVED from Swiftgram! Session will remain active, so you can restore it later.";
|
||||||
|
"SessionBackup.Notice" = "Sessions are stored in the Apple Keychain and are encrypted with your device's passcode. Sessions never leave your device.";
|
||||||
|
|
||||||
|
"MessageFilter.Title" = "Message Filter";
|
||||||
|
"MessageFilter.SubTitle" = "Remove distractions and reduce visibility of messages containing keywords below.\nKeywords are case-sensitive.";
|
||||||
|
"MessageFilter.Keywords.Title" = "Keywords";
|
||||||
|
"MessageFilter.InputPlaceholder" = "Enter keyword";
|
||||||
|
|
||||||
|
"InputToolbar.Title" = "Formatting Panel";
|
||||||
|
|
||||||
|
"Notifications.MentionsAndReplies.Title" = "@Mentions and Replies";
|
||||||
|
"Notifications.MentionsAndReplies.value.default" = "Default";
|
||||||
|
"Notifications.MentionsAndReplies.value.silenced" = "Muted";
|
||||||
|
"Notifications.MentionsAndReplies.value.disabled" = "Disabled";
|
||||||
|
"Notifications.PinnedMessages.Title" = "Pinned Messages";
|
||||||
|
"Notifications.PinnedMessages.value.default" = "Default";
|
||||||
|
"Notifications.PinnedMessages.value.silenced" = "Muted";
|
||||||
|
"Notifications.PinnedMessages.value.disabled" = "Disabled";
|
||||||
|
|
||||||
|
|
||||||
|
"PayWall.Text" = "Supercharged with Pro features";
|
||||||
|
|
||||||
|
"PayWall.SessionBackup.Title" = "Session Backup";
|
||||||
|
"PayWall.SessionBackup.Notice" = "Restore sessions from encrypted local Apple Keychain backup.";
|
||||||
|
|
||||||
|
"PayWall.MessageFilter.Title" = "Message Filter";
|
||||||
|
"PayWall.MessageFilter.Notice" = "Reduce visibility of SPAM, promotions and annoying messages.";
|
||||||
|
|
||||||
|
"PayWall.Notifications.Title" = "Disable @mentions and replies";
|
||||||
|
"PayWall.MessageFilter.Notice" = "Hide or mute non-important notifications.";
|
||||||
|
|
||||||
|
"PayWall.InputToolbar.Title" = "Formatting Panel";
|
||||||
|
"PayWall.InputToolbar.Notice" = "Save time preparing your posts with a panel right above your keyboard.";
|
||||||
|
|
||||||
|
"PayWall.RestorePurchases" = "Restore Purchases";
|
||||||
|
"PayWall.Terms" = "Terms of Service";
|
||||||
|
"PayWall.Privacy" = "Privacy Policy";
|
||||||
|
"PayWall.TermsURL" = "https://swiftgram.app/terms";
|
||||||
|
"PayWall.PrivacyURL" = "https://swiftgram.app/privacy";
|
||||||
|
"PayWall.Notice.Markdown" = "By subscribing to Swiftgram Pro you agree to the [Swiftgram Terms of Service](%1$@) and [Privacy Policy](%2$@).";
|
||||||
|
"PayWall.Notice.Raw" = "By subscribing to Swiftgram Pro you agree to the Swiftgram Terms of Service and Privacy Policy.";
|
||||||
|
|
||||||
|
"PayWall.Button.OpenPro" = "Use Pro features";
|
||||||
|
"PayWall.Button.Purchasing" = "Purchasing...";
|
||||||
|
"PayWall.Button.Restoring" = "Restoring Purchases...";
|
||||||
|
"PayWall.Button.Validating" = "Validating Purchase...";
|
||||||
|
"PayWall.Button.PaymentsUnavailable" = "Payments unavailable";
|
||||||
|
"PayWall.Button.Subscribe" = "Subscribe for %@ / month";
|
||||||
|
"PayWall.Button.ContactingAppStore" = "Contacting App Store...";
|
||||||
|
|
||||||
|
"Paywall.Error.Title" = "Error";
|
||||||
|
"PayWall.ValidationError" = "Validation Error";
|
||||||
|
"PayWall.ValidationError.TryAgain" = "Something went wrong during purchase validation. No worries! Try to Restore Purchases a bit later.";
|
||||||
|
@ -339,7 +339,10 @@ alternate_icon_folders = [
|
|||||||
"SGNeonBlue",
|
"SGNeonBlue",
|
||||||
"SGGlass",
|
"SGGlass",
|
||||||
"SGSparkling",
|
"SGSparkling",
|
||||||
"SGBeta"
|
"SGBeta",
|
||||||
|
"SGPro",
|
||||||
|
"SGGold",
|
||||||
|
"SGDucky"
|
||||||
]
|
]
|
||||||
|
|
||||||
[
|
[
|
||||||
|
@ -509,13 +509,15 @@ private struct NotificationContent: CustomStringConvertible {
|
|||||||
var isMentionOrReply: Bool
|
var isMentionOrReply: Bool
|
||||||
var isPinned: Bool = false
|
var isPinned: Bool = false
|
||||||
let chatId: Int64?
|
let chatId: Int64?
|
||||||
|
let sgStatus: SGStatus
|
||||||
|
|
||||||
var senderPerson: INPerson?
|
var senderPerson: INPerson?
|
||||||
var senderImage: INImage?
|
var senderImage: INImage?
|
||||||
|
|
||||||
var isLockedMessage: String?
|
var isLockedMessage: String?
|
||||||
|
|
||||||
init(isLockedMessage: String?, isEmpty: Bool = false, isMentionOrReply: Bool = false, chatId: Int64? = nil) {
|
init(sgStatus: SGStatus, isLockedMessage: String?, isEmpty: Bool = false, isMentionOrReply: Bool = false, chatId: Int64? = nil) {
|
||||||
|
self.sgStatus = sgStatus
|
||||||
self.isLockedMessage = isLockedMessage
|
self.isLockedMessage = isLockedMessage
|
||||||
self.isEmpty = isEmpty
|
self.isEmpty = isEmpty
|
||||||
self.isMentionOrReply = isMentionOrReply
|
self.isMentionOrReply = isMentionOrReply
|
||||||
@ -541,6 +543,7 @@ private struct NotificationContent: CustomStringConvertible {
|
|||||||
string += " isPinned: \(self.isPinned),\n"
|
string += " isPinned: \(self.isPinned),\n"
|
||||||
string += " forceIsEmpty: \(self.forceIsEmpty),\n"
|
string += " forceIsEmpty: \(self.forceIsEmpty),\n"
|
||||||
string += " forceIsSilent: \(self.forceIsSilent),\n"
|
string += " forceIsSilent: \(self.forceIsSilent),\n"
|
||||||
|
string += " sgStatus: \(self.sgStatus.status),\n"
|
||||||
string += "}"
|
string += "}"
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
@ -828,7 +831,8 @@ private final class NotificationServiceHandler {
|
|||||||
ApplicationSpecificSharedDataKeys.inAppNotificationSettings,
|
ApplicationSpecificSharedDataKeys.inAppNotificationSettings,
|
||||||
ApplicationSpecificSharedDataKeys.voiceCallSettings,
|
ApplicationSpecificSharedDataKeys.voiceCallSettings,
|
||||||
ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings,
|
ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings,
|
||||||
SharedDataKeys.loggingSettings
|
SharedDataKeys.loggingSettings,
|
||||||
|
ApplicationSpecificSharedDataKeys.sgStatus
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
@ -861,6 +865,7 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let inAppNotificationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings
|
let inAppNotificationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) ?? InAppNotificationSettings.defaultSettings
|
||||||
|
let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default
|
||||||
|
|
||||||
let voiceCallSettings: VoiceCallSettings
|
let voiceCallSettings: VoiceCallSettings
|
||||||
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) {
|
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) {
|
||||||
@ -872,7 +877,7 @@ private final class NotificationServiceHandler {
|
|||||||
guard let strongSelf = self, let recordId = recordId else {
|
guard let strongSelf = self, let recordId = recordId else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Couldn't find a matching decryption key")
|
Logger.shared.log("NotificationService \(episode)", "Couldn't find a matching decryption key")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
|
|
||||||
@ -894,7 +899,7 @@ private final class NotificationServiceHandler {
|
|||||||
guard let stateManager = stateManager else {
|
guard let stateManager = stateManager else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Didn't receive stateManager")
|
Logger.shared.log("NotificationService \(episode)", "Didn't receive stateManager")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
return
|
return
|
||||||
@ -912,7 +917,7 @@ private final class NotificationServiceHandler {
|
|||||||
settings
|
settings
|
||||||
) |> deliverOn(strongSelf.queue)).start(next: { notificationsKey, notificationSoundList in
|
) |> deliverOn(strongSelf.queue)).start(next: { notificationsKey, notificationSoundList in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
|
|
||||||
@ -921,7 +926,7 @@ private final class NotificationServiceHandler {
|
|||||||
guard let notificationsKey = notificationsKey else {
|
guard let notificationsKey = notificationsKey else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Didn't receive decryption key")
|
Logger.shared.log("NotificationService \(episode)", "Didn't receive decryption key")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
|
|
||||||
@ -930,7 +935,7 @@ private final class NotificationServiceHandler {
|
|||||||
guard let decryptedPayload = decryptedNotificationPayload(key: notificationsKey, data: payloadData) else {
|
guard let decryptedPayload = decryptedNotificationPayload(key: notificationsKey, data: payloadData) else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Couldn't decrypt payload")
|
Logger.shared.log("NotificationService \(episode)", "Couldn't decrypt payload")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
|
|
||||||
@ -939,7 +944,7 @@ private final class NotificationServiceHandler {
|
|||||||
guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [String: Any] else {
|
guard let payloadJson = try? JSONSerialization.jsonObject(with: decryptedPayload, options: []) as? [String: Any] else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Couldn't process payload as JSON")
|
Logger.shared.log("NotificationService \(episode)", "Couldn't process payload as JSON")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
|
|
||||||
@ -1047,7 +1052,7 @@ private final class NotificationServiceHandler {
|
|||||||
action = .logout
|
action = .logout
|
||||||
case "MESSAGE_MUTED":
|
case "MESSAGE_MUTED":
|
||||||
if let peerId = peerId {
|
if let peerId = peerId {
|
||||||
action = .poll(peerId: peerId, content: NotificationContent(isLockedMessage: nil, isEmpty: true, isMentionOrReply: isMentionOrReply, chatId: chatId), messageId: nil, reportDelivery: false)
|
action = .poll(peerId: peerId, content: NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true, isMentionOrReply: isMentionOrReply, chatId: chatId), messageId: nil, reportDelivery: false)
|
||||||
}
|
}
|
||||||
case "MESSAGE_DELETED":
|
case "MESSAGE_DELETED":
|
||||||
if let peerId = peerId {
|
if let peerId = peerId {
|
||||||
@ -1098,7 +1103,7 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let aps = payloadJson["aps"] as? [String: Any], var peerId = peerId {
|
if let aps = payloadJson["aps"] as? [String: Any], var peerId = peerId {
|
||||||
var content: NotificationContent = NotificationContent(isLockedMessage: isLockedMessage, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
var content: NotificationContent = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
||||||
if let alert = aps["alert"] as? [String: Any] {
|
if let alert = aps["alert"] as? [String: Any] {
|
||||||
if let topicTitleValue = payloadJson["topic_title"] as? String {
|
if let topicTitleValue = payloadJson["topic_title"] as? String {
|
||||||
topicTitle = topicTitleValue
|
topicTitle = topicTitleValue
|
||||||
@ -1249,7 +1254,7 @@ private final class NotificationServiceHandler {
|
|||||||
switch action {
|
switch action {
|
||||||
case let .call(callData):
|
case let .call(callData):
|
||||||
if let stateManager = strongSelf.stateManager {
|
if let stateManager = strongSelf.stateManager {
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
|
|
||||||
let _ = (stateManager.postbox.transaction { transaction -> String? in
|
let _ = (stateManager.postbox.transaction { transaction -> String? in
|
||||||
@ -1272,7 +1277,7 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration {
|
if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration {
|
||||||
Logger.shared.log("NotificationService \(episode)", "Will report voip notification")
|
Logger.shared.log("NotificationService \(episode)", "Will report voip notification")
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
|
|
||||||
CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in
|
CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in
|
||||||
@ -1281,7 +1286,7 @@ private final class NotificationServiceHandler {
|
|||||||
completed()
|
completed()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
var content = NotificationContent(isLockedMessage: nil)
|
var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
if let peer = callData.peer {
|
if let peer = callData.peer {
|
||||||
content.title = peer.debugDisplayTitle
|
content.title = peer.debugDisplayTitle
|
||||||
content.body = incomingCallMessage
|
content.body = incomingCallMessage
|
||||||
@ -1297,7 +1302,7 @@ private final class NotificationServiceHandler {
|
|||||||
case .logout:
|
case .logout:
|
||||||
Logger.shared.log("NotificationService \(episode)", "Will logout")
|
Logger.shared.log("NotificationService \(episode)", "Will logout")
|
||||||
|
|
||||||
let content = NotificationContent(isLockedMessage: nil, isEmpty: true)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
case let .poll(peerId, initialContent, messageId, reportDelivery):
|
case let .poll(peerId, initialContent, messageId, reportDelivery):
|
||||||
@ -1315,7 +1320,7 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
queue.async {
|
queue.async {
|
||||||
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
||||||
let content = NotificationContent(isLockedMessage: isLockedMessage)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
return
|
return
|
||||||
@ -1621,7 +1626,7 @@ private final class NotificationServiceHandler {
|
|||||||
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
||||||
|
|
||||||
if wasDisplayed {
|
if wasDisplayed {
|
||||||
content = NotificationContent(isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
||||||
Logger.shared.log("NotificationService \(episode)", "Was already displayed, skipping content")
|
Logger.shared.log("NotificationService \(episode)", "Was already displayed, skipping content")
|
||||||
} else if let messageId {
|
} else if let messageId {
|
||||||
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
||||||
@ -1708,7 +1713,7 @@ private final class NotificationServiceHandler {
|
|||||||
case let .idBased(maxIncomingReadId, _, _, _, _):
|
case let .idBased(maxIncomingReadId, _, _, _, _):
|
||||||
if maxIncomingReadId >= messageId.id {
|
if maxIncomingReadId >= messageId.id {
|
||||||
Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), skipping")
|
Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), skipping")
|
||||||
content = NotificationContent(isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isMentionOrReply: isMentionOrReply, chatId: chatId)
|
||||||
} else {
|
} else {
|
||||||
Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), not skipping")
|
Logger.shared.log("NotificationService \(episode)", "maxIncomingReadId: \(maxIncomingReadId), messageId: \(messageId.id), not skipping")
|
||||||
}
|
}
|
||||||
@ -1771,7 +1776,7 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
queue.async {
|
queue.async {
|
||||||
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
|
||||||
let content = NotificationContent(isLockedMessage: isLockedMessage, isEmpty: true)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: isLockedMessage, isEmpty: true)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
completed()
|
completed()
|
||||||
return
|
return
|
||||||
@ -1971,7 +1976,7 @@ private final class NotificationServiceHandler {
|
|||||||
|
|
||||||
var content = content
|
var content = content
|
||||||
if wasDisplayed {
|
if wasDisplayed {
|
||||||
content = NotificationContent(isLockedMessage: nil)
|
content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
} else {
|
} else {
|
||||||
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
let _ = (stateManager.postbox.transaction { transaction -> Void in
|
||||||
_internal_setStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId))
|
_internal_setStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId))
|
||||||
@ -2059,7 +2064,7 @@ private final class NotificationServiceHandler {
|
|||||||
postbox: stateManager.postbox
|
postbox: stateManager.postbox
|
||||||
)
|
)
|
||||||
|> deliverOn(strongSelf.queue)).start(next: { value in
|
|> deliverOn(strongSelf.queue)).start(next: { value in
|
||||||
var content = NotificationContent(isLockedMessage: nil, isEmpty: true)
|
var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true)
|
||||||
if isCurrentAccount {
|
if isCurrentAccount {
|
||||||
content.badge = Int(value.0)
|
content.badge = Int(value.0)
|
||||||
}
|
}
|
||||||
@ -2101,7 +2106,7 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let completeRemoval: () -> Void = {
|
let completeRemoval: () -> Void = {
|
||||||
let content = NotificationContent(isLockedMessage: nil, isEmpty: true)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true)
|
||||||
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
Logger.shared.log("NotificationService \(episode)", "Updating content to \(content)")
|
||||||
|
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
@ -2153,7 +2158,7 @@ private final class NotificationServiceHandler {
|
|||||||
postbox: stateManager.postbox
|
postbox: stateManager.postbox
|
||||||
)
|
)
|
||||||
|> deliverOn(strongSelf.queue)).start(next: { value in
|
|> deliverOn(strongSelf.queue)).start(next: { value in
|
||||||
var content = NotificationContent(isLockedMessage: nil, isEmpty: true)
|
var content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true)
|
||||||
if isCurrentAccount {
|
if isCurrentAccount {
|
||||||
content.badge = Int(value.0)
|
content.badge = Int(value.0)
|
||||||
}
|
}
|
||||||
@ -2194,7 +2199,7 @@ private final class NotificationServiceHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let completeRemoval: () -> Void = {
|
let completeRemoval: () -> Void = {
|
||||||
let content = NotificationContent(isLockedMessage: nil, isEmpty: true)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil, isEmpty: true)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
|
|
||||||
completed()
|
completed()
|
||||||
@ -2213,7 +2218,7 @@ private final class NotificationServiceHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let content = NotificationContent(isLockedMessage: nil)
|
let content = NotificationContent(sgStatus: sgStatus, isLockedMessage: nil)
|
||||||
updateCurrentContent(content)
|
updateCurrentContent(content)
|
||||||
|
|
||||||
completed()
|
completed()
|
||||||
@ -2377,7 +2382,7 @@ final class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
extension NotificationContent {
|
extension NotificationContent {
|
||||||
var forceIsEmpty: Bool {
|
var forceIsEmpty: Bool {
|
||||||
if !self.isEmpty {
|
if self.sgStatus.status > 2 && !self.isEmpty {
|
||||||
if self.isPinned {
|
if self.isPinned {
|
||||||
var desiredAction = PINNED_MESSAGE_ACTION
|
var desiredAction = PINNED_MESSAGE_ACTION
|
||||||
if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] {
|
if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] {
|
||||||
@ -2400,7 +2405,7 @@ extension NotificationContent {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var forceIsSilent: Bool {
|
var forceIsSilent: Bool {
|
||||||
if !self.silent {
|
if self.sgStatus.status > 2 && !self.silent {
|
||||||
if self.isPinned {
|
if self.isPinned {
|
||||||
var desiredAction = PINNED_MESSAGE_ACTION
|
var desiredAction = PINNED_MESSAGE_ACTION
|
||||||
if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] {
|
if let chatId = chatId, let exceptionAction = PINNED_MESSAGE_ACTION_EXCEPTIONS["\(chatId)"] {
|
||||||
|
BIN
Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png
Normal file
BIN
Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
BIN
Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@3x.png
Normal file
BIN
Telegram/Telegram-iOS/SGDucky.alticon/SGDucky@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png
Normal file
BIN
Telegram/Telegram-iOS/SGGold.alticon/SGGold@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
Telegram/Telegram-iOS/SGGold.alticon/SGGold@3x.png
Normal file
BIN
Telegram/Telegram-iOS/SGGold.alticon/SGGold@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png
Normal file
BIN
Telegram/Telegram-iOS/SGPro.alticon/SGPro@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
Telegram/Telegram-iOS/SGPro.alticon/SGPro@3x.png
Normal file
BIN
Telegram/Telegram-iOS/SGPro.alticon/SGPro@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -937,6 +937,10 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
var immediateSGStatus: SGStatus { get }
|
var immediateSGStatus: SGStatus { get }
|
||||||
var SGIAP: SGIAPManager? { get }
|
var SGIAP: SGIAPManager? { get }
|
||||||
|
func makeSGProController(context: AccountContext) -> ViewController
|
||||||
|
func makeSGPayWallController(context: AccountContext) -> ViewController?
|
||||||
|
func makeSGUpdateIOSController() -> ViewController
|
||||||
|
|
||||||
var currentInAppNotificationSettings: Atomic<InAppNotificationSettings> { get }
|
var currentInAppNotificationSettings: Atomic<InAppNotificationSettings> { get }
|
||||||
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
|
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
|
||||||
var currentStickerSettings: Atomic<StickerSettings> { get }
|
var currentStickerSettings: Atomic<StickerSettings> { get }
|
||||||
|
@ -18,7 +18,7 @@ public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool
|
|||||||
func accountWithPeer(_ context: AccountContext) -> Signal<(AccountContext, EnginePeer, Int32)?, NoError> {
|
func accountWithPeer(_ context: AccountContext) -> Signal<(AccountContext, EnginePeer, Int32)?, NoError> {
|
||||||
return combineLatest(context.account.postbox.peerView(id: context.account.peerId), renderedTotalUnreadCount(accountManager: sharedContext.accountManager, engine: context.engine))
|
return combineLatest(context.account.postbox.peerView(id: context.account.peerId), renderedTotalUnreadCount(accountManager: sharedContext.accountManager, engine: context.engine))
|
||||||
|> map { view, totalUnreadCount -> (EnginePeer?, Int32) in
|
|> map { view, totalUnreadCount -> (EnginePeer?, Int32) in
|
||||||
return (view.peers[view.peerId].flatMap(EnginePeer.init) ?? EnginePeer.init(TelegramUser(id: view.peerId, accessHash: nil, firstName: "IMPORTED", lastName: "\(view.peerId.id._internalGetInt64Value())", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags(), emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)), totalUnreadCount.0)
|
return (view.peers[view.peerId].flatMap(EnginePeer.init) ?? EnginePeer.init(TelegramUser(id: view.peerId, accessHash: nil, firstName: "RESTORED", lastName: "\(view.peerId.id._internalGetInt64Value())", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags(), emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)), totalUnreadCount.0)
|
||||||
}
|
}
|
||||||
|> distinctUntilChanged { lhs, rhs in
|
|> distinctUntilChanged { lhs, rhs in
|
||||||
if lhs.0 != rhs.0 {
|
if lhs.0 != rhs.0 {
|
||||||
|
@ -509,7 +509,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
self.initToolbarIfNeeded()
|
self.initToolbarIfNeeded(context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var sendPressed: ((NSAttributedString?) -> Void)?
|
public var sendPressed: ((NSAttributedString?) -> Void)?
|
||||||
@ -636,7 +636,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
textInputNode.view.addGestureRecognizer(recognizer)
|
textInputNode.view.addGestureRecognizer(recognizer)
|
||||||
|
|
||||||
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
||||||
self.initToolbarIfNeeded()
|
self.initToolbarIfNeeded(context: self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat {
|
||||||
@ -1914,10 +1914,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
|||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
extension AttachmentTextInputPanelNode {
|
extension AttachmentTextInputPanelNode {
|
||||||
|
|
||||||
func initToolbarIfNeeded() {
|
func initToolbarIfNeeded(context: AccountContext) {
|
||||||
guard #available(iOS 13.0, *) else { return }
|
guard #available(iOS 13.0, *) else { return }
|
||||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||||
guard SGSimpleSettings.shared.b else { return }
|
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
|
||||||
guard self.toolbarNode == nil else { return }
|
guard self.toolbarNode == nil else { return }
|
||||||
let toolbarView = ChatToolbarView(
|
let toolbarView = ChatToolbarView(
|
||||||
onQuote: { [weak self] in
|
onQuote: { [weak self] in
|
||||||
|
@ -394,6 +394,15 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
case "SGBeta":
|
case "SGBeta":
|
||||||
name = "β Beta"
|
name = "β Beta"
|
||||||
bordered = false
|
bordered = false
|
||||||
|
case "SGPro":
|
||||||
|
name = "Pro"
|
||||||
|
bordered = false
|
||||||
|
case "SGGold":
|
||||||
|
name = "Gold"
|
||||||
|
bordered = false
|
||||||
|
case "SGDucky":
|
||||||
|
name = "Ducky"
|
||||||
|
bordered = false
|
||||||
case "BlueIcon":
|
case "BlueIcon":
|
||||||
name = item.strings.Appearance_AppIconDefault
|
name = item.strings.Appearance_AppIconDefault
|
||||||
case "BlackIcon":
|
case "BlackIcon":
|
||||||
@ -424,7 +433,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
name = icon.name
|
name = icon.name
|
||||||
}
|
}
|
||||||
|
|
||||||
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: {
|
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isSGPro, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: {
|
||||||
item.updated(icon)
|
item.updated(icon)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -567,6 +567,13 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
|
|||||||
controller?.replace(with: c)
|
controller?.replace(with: c)
|
||||||
}
|
}
|
||||||
pushControllerImpl?(controller)
|
pushControllerImpl?(controller)
|
||||||
|
// MARK: Swiftgram
|
||||||
|
} else if icon.isSGPro && context.sharedContext.immediateSGStatus.status < 2 {
|
||||||
|
if let payWallController = context.sharedContext.makeSGPayWallController(context: context) {
|
||||||
|
presentControllerImpl?(payWallController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||||
|
} else {
|
||||||
|
presentControllerImpl?(context.sharedContext.makeSGUpdateIOSController(), nil)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentAppIconName.set(icon.name)
|
currentAppIconName.set(icon.name)
|
||||||
context.sharedContext.applicationBindings.requestSetAlternateIconName(icon.name, { _ in
|
context.sharedContext.applicationBindings.requestSetAlternateIconName(icon.name, { _ in
|
||||||
@ -1027,12 +1034,14 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)))
|
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings, ApplicationSpecificSharedDataKeys.sgStatus]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)))
|
||||||
|> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
|
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
|
||||||
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings
|
let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings
|
||||||
|
|
||||||
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
|
// MARK: Swiftgram
|
||||||
|
let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default
|
||||||
|
let isPremium = sgStatus.status > 1
|
||||||
|
|
||||||
let themeReference: PresentationThemeReference
|
let themeReference: PresentationThemeReference
|
||||||
if presentationData.autoNightModeTriggered {
|
if presentationData.autoNightModeTriggered {
|
||||||
|
@ -46,8 +46,10 @@ public struct PresentationAppIcon: Equatable {
|
|||||||
public let imageName: String
|
public let imageName: String
|
||||||
public let isDefault: Bool
|
public let isDefault: Bool
|
||||||
public let isPremium: Bool
|
public let isPremium: Bool
|
||||||
|
public let isSGPro: Bool
|
||||||
|
|
||||||
public init(name: String, imageName: String, isDefault: Bool = false, isPremium: Bool = false) {
|
public init(isSGPro: Bool = false, name: String, imageName: String, isDefault: Bool = false, isPremium: Bool = false) {
|
||||||
|
self.isSGPro = isSGPro
|
||||||
self.name = name
|
self.name = name
|
||||||
self.imageName = imageName
|
self.imageName = imageName
|
||||||
self.isDefault = isDefault
|
self.isDefault = isDefault
|
||||||
|
@ -64,6 +64,7 @@ private func renderIcon(name: String, scaleFactor: CGFloat = 1.0, backgroundColo
|
|||||||
|
|
||||||
public struct PresentationResourcesSettings {
|
public struct PresentationResourcesSettings {
|
||||||
public static let swiftgram = renderIcon(name: "SwiftgramSettings", scaleFactor: 30.0 / 512.0)
|
public static let swiftgram = renderIcon(name: "SwiftgramSettings", scaleFactor: 30.0 / 512.0)
|
||||||
|
public static let swiftgramPro = renderIcon(name: "SwiftgramPro", scaleFactor: 30.0 / 256.0)
|
||||||
public static let editProfile = renderIcon(name: "Settings/Menu/EditProfile")
|
public static let editProfile = renderIcon(name: "Settings/Menu/EditProfile")
|
||||||
public static let proxy = renderIcon(name: "Settings/Menu/Proxy")
|
public static let proxy = renderIcon(name: "Settings/Menu/Proxy")
|
||||||
public static let savedMessages = renderIcon(name: "Settings/Menu/SavedMessages")
|
public static let savedMessages = renderIcon(name: "Settings/Menu/SavedMessages")
|
||||||
|
@ -19,7 +19,9 @@ sgdeps = [
|
|||||||
"//Swiftgram/SGDebugUI:SGDebugUI",
|
"//Swiftgram/SGDebugUI:SGDebugUI",
|
||||||
"//Swiftgram/SGInputToolbar:SGInputToolbar",
|
"//Swiftgram/SGInputToolbar:SGInputToolbar",
|
||||||
"//Swiftgram/SGIAP:SGIAP",
|
"//Swiftgram/SGIAP:SGIAP",
|
||||||
"//Swiftgram/SGPayWall:SGPayWall"
|
"//Swiftgram/SGPayWall:SGPayWall",
|
||||||
|
"//Swiftgram/SGProUI:SGProUI",
|
||||||
|
"//Swiftgram/SGKeychainBackupManager:SGKeychainBackupManager",
|
||||||
# "//Swiftgram/SGContentAnalysis:SGContentAnalysis"
|
# "//Swiftgram/SGContentAnalysis:SGContentAnalysis"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -693,7 +693,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
|||||||
open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
|
open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
if !self.wasFilteredKeywordTested && !SGSimpleSettings.shared.messageFilterKeywords.isEmpty {
|
if !self.wasFilteredKeywordTested && !SGSimpleSettings.shared.messageFilterKeywords.isEmpty && SGSimpleSettings.shared.ephemeralStatus > 1 {
|
||||||
let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId)
|
let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||||
if incomingMessage {
|
if incomingMessage {
|
||||||
if let matchedKeyword = SGSimpleSettings.shared.messageFilterKeywords.first(where: { item.message.text.contains($0) }) {
|
if let matchedKeyword = SGSimpleSettings.shared.messageFilterKeywords.first(where: { item.message.text.contains($0) }) {
|
||||||
|
@ -528,8 +528,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public var likeIconView: UIView? {
|
public var likeIconView: UIView? {
|
||||||
return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView
|
return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView
|
||||||
}
|
}
|
||||||
|
// MARK: Swifgtram
|
||||||
override init(frame: CGRect) {
|
init(context: AccountContext, frame: CGRect) {
|
||||||
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
|
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
|
||||||
|
|
||||||
self.vibrancyEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
|
self.vibrancyEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
|
||||||
@ -573,7 +573,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
self.initToolbarIfNeeded()
|
self.initToolbarIfNeeded(context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -2277,7 +2277,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func makeView() -> View {
|
public func makeView() -> View {
|
||||||
return View(frame: CGRect())
|
return View(context: self.context, frame: CGRect())
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
@ -2328,10 +2328,10 @@ final class ViewForOverlayContent: UIView {
|
|||||||
|
|
||||||
|
|
||||||
extension MessageInputPanelComponent.View {
|
extension MessageInputPanelComponent.View {
|
||||||
func initToolbarIfNeeded() {
|
func initToolbarIfNeeded(context: AccountContext) {
|
||||||
guard #available(iOS 13.0, *) else { return }
|
guard #available(iOS 13.0, *) else { return }
|
||||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||||
guard SGSimpleSettings.shared.b else { return }
|
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
|
||||||
guard self.toolbarView == nil else { return }
|
guard self.toolbarView == nil else { return }
|
||||||
let notificationName = Notification.Name("sgToolbarAction")
|
let notificationName = Notification.Name("sgToolbarAction")
|
||||||
let toolbar = ChatToolbarView(
|
let toolbar = ChatToolbarView(
|
||||||
|
@ -504,6 +504,7 @@ private enum PeerInfoContextSubject {
|
|||||||
|
|
||||||
private enum PeerInfoSettingsSection {
|
private enum PeerInfoSettingsSection {
|
||||||
case swiftgram
|
case swiftgram
|
||||||
|
case swiftgramPro
|
||||||
case avatar
|
case avatar
|
||||||
case edit
|
case edit
|
||||||
case proxy
|
case proxy
|
||||||
@ -947,7 +948,6 @@ private func settingsItems(showProfileId: Bool, data: PeerInfoScreenData?, conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sgSectionId = 0
|
|
||||||
// let locale = presentationData.strings.baseLanguageCode
|
// let locale = presentationData.strings.baseLanguageCode
|
||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
let hasNewSGFeatures = {
|
let hasNewSGFeatures = {
|
||||||
@ -960,7 +960,10 @@ private func settingsItems(showProfileId: Bool, data: PeerInfoScreenData?, conte
|
|||||||
swiftgramLabel = .none
|
swiftgramLabel = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: sgSectionId, label: swiftgramLabel, text: "Swiftgram", icon: PresentationResourcesSettings.swiftgram, action: {
|
items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: 0, label: .titleBadge(presentationData.strings.Settings_New, presentationData.theme.list.itemAccentColor), text: "Swiftgram Pro", icon: PresentationResourcesSettings.swiftgramPro, action: {
|
||||||
|
interaction.openSettings(.swiftgramPro)
|
||||||
|
}))
|
||||||
|
items[.swiftgram]!.append(PeerInfoScreenDisclosureItem(id: 1, label: swiftgramLabel, text: "Swiftgram", icon: PresentationResourcesSettings.swiftgram, action: {
|
||||||
interaction.openSettings(.swiftgram)
|
interaction.openSettings(.swiftgram)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -10247,6 +10250,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
switch section {
|
switch section {
|
||||||
case .swiftgram:
|
case .swiftgram:
|
||||||
self.controller?.push(sgSettingsController(context: self.context))
|
self.controller?.push(sgSettingsController(context: self.context))
|
||||||
|
case .swiftgramPro:
|
||||||
|
if self.context.sharedContext.immediateSGStatus.status > 1 {
|
||||||
|
self.controller?.push(self.context.sharedContext.makeSGProController(context: self.context))
|
||||||
|
} else {
|
||||||
|
if let payWallController = self.context.sharedContext.makeSGPayWallController(context: self.context) {
|
||||||
|
self.controller?.present(payWallController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||||
|
} else {
|
||||||
|
self.controller?.present(self.context.sharedContext.makeSGUpdateIOSController(), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
case .avatar:
|
case .avatar:
|
||||||
self.controller?.openAvatarForEditing()
|
self.controller?.openAvatarForEditing()
|
||||||
case .edit:
|
case .edit:
|
||||||
|
@ -850,6 +850,10 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
PresentationAppIcon(name: "SGNight", imageName: "SGNight"),
|
PresentationAppIcon(name: "SGNight", imageName: "SGNight"),
|
||||||
PresentationAppIcon(name: "SGSky", imageName: "SGSky"),
|
PresentationAppIcon(name: "SGSky", imageName: "SGSky"),
|
||||||
PresentationAppIcon(name: "SGTitanium", imageName: "SGTitanium"),
|
PresentationAppIcon(name: "SGTitanium", imageName: "SGTitanium"),
|
||||||
|
PresentationAppIcon(isSGPro: true, name: "SGPro", imageName: "SGPro"),
|
||||||
|
PresentationAppIcon(isSGPro: true, name: "SGGold", imageName: "SGGold"),
|
||||||
|
PresentationAppIcon(isSGPro: true, name: "SGDucky", imageName: "SGDucky"),
|
||||||
|
PresentationAppIcon(name: "", imageName: ""), // Empty
|
||||||
PresentationAppIcon(name: "SGNeon", imageName: "SGNeon"),
|
PresentationAppIcon(name: "SGNeon", imageName: "SGNeon"),
|
||||||
PresentationAppIcon(name: "SGNeonBlue", imageName: "SGNeonBlue"),
|
PresentationAppIcon(name: "SGNeonBlue", imageName: "SGNeonBlue"),
|
||||||
PresentationAppIcon(name: "SGGlass", imageName: "SGGlass"),
|
PresentationAppIcon(name: "SGGlass", imageName: "SGGlass"),
|
||||||
|
@ -941,7 +941,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
|
|||||||
self.addSubnode(self.clippingNode)
|
self.addSubnode(self.clippingNode)
|
||||||
|
|
||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
self.initToolbarIfNeeded()
|
self.initToolbarIfNeeded(context: context)
|
||||||
|
|
||||||
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
|
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -5064,10 +5064,10 @@ private final class BoostSlowModeButton: HighlightTrackingButtonNode {
|
|||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
extension ChatTextInputPanelNode {
|
extension ChatTextInputPanelNode {
|
||||||
|
|
||||||
func initToolbarIfNeeded() {
|
func initToolbarIfNeeded(context: AccountContext) {
|
||||||
guard #available(iOS 13.0, *) else { return }
|
guard #available(iOS 13.0, *) else { return }
|
||||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||||
guard SGSimpleSettings.shared.b else { return }
|
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
|
||||||
guard self.toolbarNode == nil else { return }
|
guard self.toolbarNode == nil else { return }
|
||||||
let toolbarView = ChatToolbarView(
|
let toolbarView = ChatToolbarView(
|
||||||
onQuote: { [weak self] in
|
onQuote: { [weak self] in
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
// MARK: Swiftgram
|
// MARK: Swiftgram
|
||||||
import SGIAP
|
import SGIAP
|
||||||
|
import SGPayWall
|
||||||
|
import SGProUI
|
||||||
|
import SGSimpleSettings
|
||||||
|
//
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
@ -484,6 +488,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
|> deliverOnMainQueue).start(next: { sharedData in
|
|> deliverOnMainQueue).start(next: { sharedData in
|
||||||
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) {
|
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) {
|
||||||
let _ = immediateSGStatusValue.swap(settings)
|
let _ = immediateSGStatusValue.swap(settings)
|
||||||
|
SGSimpleSettings.shared.ephemeralStatus = settings.status
|
||||||
|
SGSimpleSettings.shared.status = settings.status
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
self.initSGIAP(isMainApp: applicationBindings.isMainApp)
|
self.initSGIAP(isMainApp: applicationBindings.isMainApp)
|
||||||
@ -3050,4 +3056,39 @@ extension SharedAccountContextImpl {
|
|||||||
self.SGIAP = nil
|
self.SGIAP = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func makeSGProController(context: AccountContext) -> ViewController {
|
||||||
|
let controller = sgProController(context: context)
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeSGPayWallController(context: AccountContext) -> ViewController? {
|
||||||
|
guard #available(iOS 13.0, *) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let sgIAP = self.SGIAP else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusSignal = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus])
|
||||||
|
|> map { sharedData -> Int64 in
|
||||||
|
let sgStatus = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) ?? SGStatus.default
|
||||||
|
return sgStatus.status
|
||||||
|
}
|
||||||
|
|
||||||
|
let proController = self.makeSGProController(context: context)
|
||||||
|
let payWallController = sgPayWallController(statusSignal: statusSignal, replacementController: proController, presentationData: self.currentPresentationData.with { $0 }, SGIAPManager: sgIAP, openUrl: self.applicationBindings.openUrl)
|
||||||
|
return payWallController
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeSGUpdateIOSController() -> ViewController {
|
||||||
|
let presentationData = self.currentPresentationData.with { $0 }
|
||||||
|
let controller = UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(title: nil, text: "Common.UpdateOS".i18n(presentationData.strings.baseLanguageCode), timeout: nil, customUndoText: nil),
|
||||||
|
elevatedLayout: false,
|
||||||
|
action: { _ in return false }
|
||||||
|
)
|
||||||
|
return controller
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user