mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-06 21:35:53 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
833 lines
41 KiB
Swift
833 lines
41 KiB
Swift
import SGStrings
|
||
import SGSimpleSettings
|
||
|
||
import Foundation
|
||
import SwiftSignalKit
|
||
import UIKit
|
||
import Postbox
|
||
import TelegramCore
|
||
import Display
|
||
import DeviceAccess
|
||
import TelegramPresentationData
|
||
import AccountContext
|
||
import LiveLocationManager
|
||
import TemporaryCachedPeerDataManager
|
||
import PhoneNumberFormat
|
||
import TelegramUIPreferences
|
||
import TelegramVoip
|
||
import TelegramCallsUI
|
||
import TelegramBaseController
|
||
import AsyncDisplayKit
|
||
import PresentationDataUtils
|
||
import FetchManagerImpl
|
||
import InAppPurchaseManager
|
||
import AnimationCache
|
||
import MultiAnimationRenderer
|
||
import AppBundle
|
||
import DirectMediaImageCache
|
||
|
||
private final class DeviceSpecificContactImportContext {
|
||
let disposable = MetaDisposable()
|
||
var reference: DeviceContactBasicDataWithReference?
|
||
|
||
init() {
|
||
}
|
||
|
||
deinit {
|
||
self.disposable.dispose()
|
||
}
|
||
}
|
||
|
||
private final class DeviceSpecificContactImportContexts {
|
||
private let queue: Queue
|
||
|
||
private var contexts: [PeerId: DeviceSpecificContactImportContext] = [:]
|
||
|
||
init(queue: Queue) {
|
||
self.queue = queue
|
||
}
|
||
|
||
deinit {
|
||
assert(self.queue.isCurrent())
|
||
}
|
||
|
||
func update(account: Account, deviceContactDataManager: DeviceContactDataManager, references: [PeerId: DeviceContactBasicDataWithReference]) {
|
||
var validIds = Set<PeerId>()
|
||
for (peerId, reference) in references {
|
||
validIds.insert(peerId)
|
||
|
||
let context: DeviceSpecificContactImportContext
|
||
if let current = self.contexts[peerId] {
|
||
context = current
|
||
} else {
|
||
context = DeviceSpecificContactImportContext()
|
||
self.contexts[peerId] = context
|
||
}
|
||
if context.reference != reference {
|
||
context.reference = reference
|
||
|
||
let signal = TelegramEngine(account: account).data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||
|> map { peer -> String? in
|
||
if case let .user(user) = peer {
|
||
return user.phone
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|> distinctUntilChanged
|
||
|> mapToSignal { phone -> Signal<Never, NoError> in
|
||
guard let phone = phone else {
|
||
return .complete()
|
||
}
|
||
var found = false
|
||
let formattedPhone = formatPhoneNumber(phone)
|
||
for number in reference.basicData.phoneNumbers {
|
||
if formatPhoneNumber(number.value) == formattedPhone {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return deviceContactDataManager.appendPhoneNumber(DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: formattedPhone), to: reference.stableId)
|
||
|> ignoreValues
|
||
} else {
|
||
return .complete()
|
||
}
|
||
}
|
||
context.disposable.set(signal.start())
|
||
}
|
||
}
|
||
|
||
var removeIds: [PeerId] = []
|
||
for peerId in self.contexts.keys {
|
||
if !validIds.contains(peerId) {
|
||
removeIds.append(peerId)
|
||
}
|
||
}
|
||
for peerId in removeIds {
|
||
self.contexts.removeValue(forKey: peerId)
|
||
}
|
||
}
|
||
}
|
||
|
||
public final class AccountContextImpl: AccountContext {
|
||
public let sharedContextImpl: SharedAccountContextImpl
|
||
public var sharedContext: SharedAccountContext {
|
||
return self.sharedContextImpl
|
||
}
|
||
public let account: Account
|
||
public let engine: TelegramEngine
|
||
|
||
public let fetchManager: FetchManager
|
||
public let prefetchManager: PrefetchManager?
|
||
|
||
public var keyShortcutsController: KeyShortcutsController?
|
||
|
||
public let downloadedMediaStoreManager: DownloadedMediaStoreManager
|
||
|
||
public let liveLocationManager: LiveLocationManager?
|
||
public let peersNearbyManager: PeersNearbyManager?
|
||
public let wallpaperUploadManager: WallpaperUploadManager?
|
||
private let themeUpdateManager: ThemeUpdateManager?
|
||
public let inAppPurchaseManager: InAppPurchaseManager?
|
||
public let starsContext: StarsContext?
|
||
|
||
public let peerChannelMemberCategoriesContextsManager = PeerChannelMemberCategoriesContextsManager()
|
||
|
||
public let currentLimitsConfiguration: Atomic<LimitsConfiguration>
|
||
private let _limitsConfiguration = Promise<LimitsConfiguration>()
|
||
public var limitsConfiguration: Signal<LimitsConfiguration, NoError> {
|
||
return self._limitsConfiguration.get()
|
||
}
|
||
|
||
public var currentContentSettings: Atomic<ContentSettings>
|
||
private let _contentSettings = Promise<ContentSettings>()
|
||
public var contentSettings: Signal<ContentSettings, NoError> {
|
||
return self._contentSettings.get()
|
||
}
|
||
|
||
public var currentAppConfiguration: Atomic<AppConfiguration>
|
||
private let _appConfiguration = Promise<AppConfiguration>()
|
||
public var appConfiguration: Signal<AppConfiguration, NoError> {
|
||
return self._appConfiguration.get()
|
||
}
|
||
|
||
public var currentCountriesConfiguration: Atomic<CountriesConfiguration>
|
||
private let _countriesConfiguration = Promise<CountriesConfiguration>()
|
||
public var countriesConfiguration: Signal<CountriesConfiguration, NoError> {
|
||
return self._countriesConfiguration.get()
|
||
}
|
||
|
||
public var watchManager: WatchManager?
|
||
|
||
private var storedPassword: (String, CFAbsoluteTime, SwiftSignalKit.Timer)?
|
||
private var limitsConfigurationDisposable: Disposable?
|
||
private var contentSettingsDisposable: Disposable?
|
||
private var appConfigurationDisposable: Disposable?
|
||
private var countriesConfigurationDisposable: Disposable?
|
||
|
||
private let deviceSpecificContactImportContexts: QueueLocalObject<DeviceSpecificContactImportContexts>
|
||
private var managedAppSpecificContactsDisposable: Disposable?
|
||
|
||
private var experimentalUISettingsDisposable: Disposable?
|
||
|
||
public let cachedGroupCallContexts: AccountGroupCallContextCache
|
||
|
||
public let animationCache: AnimationCache
|
||
public let animationRenderer: MultiAnimationRenderer
|
||
|
||
private var animatedEmojiStickersDisposable: Disposable?
|
||
public private(set) var animatedEmojiStickersValue: [String: [StickerPackItem]] = [:]
|
||
private let animatedEmojiStickersPromise = Promise<[String: [StickerPackItem]]>()
|
||
public var animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> {
|
||
return self.animatedEmojiStickersPromise.get()
|
||
}
|
||
|
||
private var additionalAnimatedEmojiStickersPromise: Promise<[String: [Int: StickerPackItem]]>?
|
||
public var additionalAnimatedEmojiStickers: Signal<[String: [Int: StickerPackItem]], NoError> {
|
||
let additionalAnimatedEmojiStickersPromise: Promise<[String: [Int: StickerPackItem]]>
|
||
if let current = self.additionalAnimatedEmojiStickersPromise {
|
||
additionalAnimatedEmojiStickersPromise = current
|
||
} else {
|
||
additionalAnimatedEmojiStickersPromise = Promise<[String: [Int: StickerPackItem]]>()
|
||
self.additionalAnimatedEmojiStickersPromise = additionalAnimatedEmojiStickersPromise
|
||
additionalAnimatedEmojiStickersPromise.set(self.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false)
|
||
|> map { animatedEmoji -> [String: [Int: StickerPackItem]] in
|
||
let sequence = "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣".strippedEmoji
|
||
var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:]
|
||
switch animatedEmoji {
|
||
case let .result(_, items, _):
|
||
for item in items {
|
||
let indexKeys = item.getStringRepresentationsOfIndexKeys()
|
||
if indexKeys.count > 1, let first = indexKeys.first, let last = indexKeys.last {
|
||
let emoji: String?
|
||
let indexEmoji: String?
|
||
if sequence.contains(first.strippedEmoji) {
|
||
emoji = last
|
||
indexEmoji = first
|
||
} else if sequence.contains(last.strippedEmoji) {
|
||
emoji = first
|
||
indexEmoji = last
|
||
} else {
|
||
emoji = nil
|
||
indexEmoji = nil
|
||
}
|
||
|
||
if let emoji = emoji?.strippedEmoji, let indexEmoji = indexEmoji?.strippedEmoji.first, let strIndex = sequence.firstIndex(of: indexEmoji) {
|
||
let index = sequence.distance(from: sequence.startIndex, to: strIndex)
|
||
if animatedEmojiStickers[emoji] != nil {
|
||
animatedEmojiStickers[emoji]![index] = item
|
||
} else {
|
||
animatedEmojiStickers[emoji] = [index: item]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
return animatedEmojiStickers
|
||
})
|
||
}
|
||
return additionalAnimatedEmojiStickersPromise.get()
|
||
}
|
||
|
||
private var availableReactionsValue: Promise<AvailableReactions?>?
|
||
public var availableReactions: Signal<AvailableReactions?, NoError> {
|
||
let availableReactionsValue: Promise<AvailableReactions?>
|
||
if let current = self.availableReactionsValue {
|
||
availableReactionsValue = current
|
||
} else {
|
||
availableReactionsValue = Promise<AvailableReactions?>()
|
||
self.availableReactionsValue = availableReactionsValue
|
||
availableReactionsValue.set(self.engine.stickers.availableReactions())
|
||
}
|
||
return availableReactionsValue.get()
|
||
}
|
||
|
||
private var availableMessageEffectsValue: Promise<AvailableMessageEffects?>?
|
||
public var availableMessageEffects: Signal<AvailableMessageEffects?, NoError> {
|
||
let availableMessageEffectsValue: Promise<AvailableMessageEffects?>
|
||
if let current = self.availableMessageEffectsValue {
|
||
availableMessageEffectsValue = current
|
||
} else {
|
||
availableMessageEffectsValue = Promise<AvailableMessageEffects?>()
|
||
self.availableMessageEffectsValue = availableMessageEffectsValue
|
||
availableMessageEffectsValue.set(self.engine.stickers.availableMessageEffects())
|
||
}
|
||
return availableMessageEffectsValue.get()
|
||
}
|
||
|
||
private var userLimitsConfigurationDisposable: Disposable?
|
||
public private(set) var userLimits: EngineConfiguration.UserLimits
|
||
|
||
private var peerNameColorsConfigurationDisposable: Disposable?
|
||
public private(set) var peerNameColors: PeerNameColors
|
||
|
||
private var audioTranscriptionTrialDisposable: Disposable?
|
||
public private(set) var audioTranscriptionTrial: AudioTranscription.TrialState
|
||
|
||
public private(set) var isPremium: Bool
|
||
|
||
public let imageCache: AnyObject?
|
||
|
||
public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions, temp: Bool = false)
|
||
{
|
||
self.sharedContextImpl = sharedContext
|
||
self.account = account
|
||
self.engine = TelegramEngine(account: account)
|
||
|
||
self.imageCache = DirectMediaImageCache(account: account)
|
||
|
||
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
|
||
self.peerNameColors = PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors)
|
||
self.audioTranscriptionTrial = AudioTranscription.TrialState.defaultValue
|
||
self.isPremium = false
|
||
|
||
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
|
||
|
||
if let locationManager = self.sharedContextImpl.locationManager {
|
||
self.liveLocationManager = LiveLocationManagerImpl(engine: self.engine, locationManager: locationManager, inForeground: sharedContext.applicationBindings.applicationInForeground)
|
||
} else {
|
||
self.liveLocationManager = nil
|
||
}
|
||
self.fetchManager = FetchManagerImpl(postbox: account.postbox, storeManager: self.downloadedMediaStoreManager)
|
||
if sharedContext.applicationBindings.isMainApp && !temp {
|
||
self.prefetchManager = PrefetchManagerImpl(sharedContext: sharedContext, account: account, engine: self.engine, fetchManager: self.fetchManager)
|
||
self.wallpaperUploadManager = WallpaperUploadManagerImpl(sharedContext: sharedContext, account: account, presentationData: sharedContext.presentationData)
|
||
self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account)
|
||
|
||
self.inAppPurchaseManager = InAppPurchaseManager(engine: self.engine)
|
||
self.starsContext = self.engine.payments.peerStarsContext()
|
||
} else {
|
||
self.prefetchManager = nil
|
||
self.wallpaperUploadManager = nil
|
||
self.themeUpdateManager = nil
|
||
self.inAppPurchaseManager = nil
|
||
self.starsContext = nil
|
||
}
|
||
|
||
self.account.stateManager.starsContext = self.starsContext
|
||
|
||
self.peersNearbyManager = nil
|
||
|
||
self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl()
|
||
|
||
let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox
|
||
self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
|
||
return TempBox.shared.tempFile(fileName: "file").path
|
||
}, updateStorageStats: { path, size in
|
||
if let pathData = path.data(using: .utf8) {
|
||
cacheStorageBox.update(id: pathData, size: size)
|
||
}
|
||
})
|
||
self.animationRenderer = MultiAnimationRendererImpl()
|
||
|
||
let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration])
|
||
|> map { preferences -> LimitsConfiguration in
|
||
return preferences.values[PreferencesKeys.limitsConfiguration]?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue
|
||
}
|
||
|
||
self.currentLimitsConfiguration = Atomic(value: limitsConfiguration)
|
||
self._limitsConfiguration.set(.single(limitsConfiguration) |> then(updatedLimitsConfiguration))
|
||
|
||
let currentLimitsConfiguration = self.currentLimitsConfiguration
|
||
self.limitsConfigurationDisposable = (self._limitsConfiguration.get()
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
let _ = currentLimitsConfiguration.swap(value)
|
||
})
|
||
|
||
let updatedContentSettings = getContentSettings(postbox: account.postbox)
|
||
self.currentContentSettings = Atomic(value: contentSettings)
|
||
self._contentSettings.set(.single(contentSettings) |> then(updatedContentSettings))
|
||
|
||
let currentContentSettings = self.currentContentSettings
|
||
self.contentSettingsDisposable = (self._contentSettings.get()
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
let _ = currentContentSettings.swap(value)
|
||
})
|
||
|
||
let updatedAppConfiguration = getAppConfiguration(postbox: account.postbox)
|
||
self.currentAppConfiguration = Atomic(value: appConfiguration)
|
||
self._appConfiguration.set(.single(appConfiguration) |> then(updatedAppConfiguration))
|
||
|
||
let currentAppConfiguration = self.currentAppConfiguration
|
||
self.appConfigurationDisposable = (self._appConfiguration.get()
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
let _ = currentAppConfiguration.swap(value)
|
||
})
|
||
|
||
self.currentCountriesConfiguration = Atomic(value: CountriesConfiguration(countries: loadCountryCodes()))
|
||
if !temp {
|
||
let currentCountriesConfiguration = self.currentCountriesConfiguration
|
||
self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: nil)
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value))
|
||
})
|
||
}
|
||
|
||
let queue = Queue()
|
||
self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: {
|
||
return DeviceSpecificContactImportContexts(queue: queue)
|
||
})
|
||
|
||
if let contactDataManager = sharedContext.contactDataManager {
|
||
let deviceSpecificContactImportContexts = self.deviceSpecificContactImportContexts
|
||
self.managedAppSpecificContactsDisposable = (contactDataManager.appSpecificReferences()
|
||
|> deliverOn(queue)).start(next: { appSpecificReferences in
|
||
deviceSpecificContactImportContexts.with { context in
|
||
context.update(account: account, deviceContactDataManager: contactDataManager, references: appSpecificReferences)
|
||
}
|
||
})
|
||
}
|
||
|
||
account.callSessionManager.updateVersions(versions: PresentationCallManagerImpl.voipVersions(includeExperimental: true, includeReference: true).map { version, supportsVideo -> CallSessionManagerImplementationVersion in
|
||
CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo)
|
||
})
|
||
|
||
self.animatedEmojiStickersDisposable = (self.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||
switch animatedEmoji {
|
||
case let .result(_, items, _):
|
||
for item in items {
|
||
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
||
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
|
||
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
|
||
if animatedEmojiStickers[strippedEmoji] == nil {
|
||
animatedEmojiStickers[strippedEmoji] = [item]
|
||
}
|
||
}
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
return animatedEmojiStickers
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] stickers in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.animatedEmojiStickersValue = stickers
|
||
strongSelf.animatedEmojiStickersPromise.set(.single(stickers))
|
||
})
|
||
|
||
self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId))
|
||
|> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in
|
||
let isPremium = peer?.isPremium ?? false
|
||
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium))
|
||
|> map { userLimits in
|
||
return (isPremium, userLimits)
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).startStrict(next: { [weak self] isPremium, userLimits in
|
||
guard let self = self else {
|
||
return
|
||
}
|
||
self.isPremium = isPremium
|
||
self.userLimits = userLimits
|
||
})
|
||
|
||
self.peerNameColorsConfigurationDisposable = (combineLatest(
|
||
self.engine.accountData.observeAvailableColorOptions(scope: .replies),
|
||
self.engine.accountData.observeAvailableColorOptions(scope: .profile)
|
||
)
|
||
|> deliverOnMainQueue).startStrict(next: { [weak self] availableReplyColors, availableProfileColors in
|
||
guard let self = self else {
|
||
return
|
||
}
|
||
self.peerNameColors = PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors)
|
||
})
|
||
|
||
self.audioTranscriptionTrialDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId))
|
||
|> mapToSignal { peer -> Signal<AudioTranscription.TrialState, NoError> in
|
||
let isPremium = peer?.isPremium ?? false
|
||
if isPremium {
|
||
return .single(AudioTranscription.TrialState(cooldownUntilTime: nil, remainingCount: 1))
|
||
} else {
|
||
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.AudioTranscriptionTrial())
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).startStrict(next: { [weak self] audioTranscriptionTrial in
|
||
guard let self = self else {
|
||
return
|
||
}
|
||
self.audioTranscriptionTrial = audioTranscriptionTrial
|
||
})
|
||
}
|
||
|
||
deinit {
|
||
self.limitsConfigurationDisposable?.dispose()
|
||
self.managedAppSpecificContactsDisposable?.dispose()
|
||
self.contentSettingsDisposable?.dispose()
|
||
self.appConfigurationDisposable?.dispose()
|
||
self.countriesConfigurationDisposable?.dispose()
|
||
self.experimentalUISettingsDisposable?.dispose()
|
||
self.animatedEmojiStickersDisposable?.dispose()
|
||
self.userLimitsConfigurationDisposable?.dispose()
|
||
self.peerNameColorsConfigurationDisposable?.dispose()
|
||
}
|
||
|
||
public func storeSecureIdPassword(password: String) {
|
||
self.storedPassword?.2.invalidate()
|
||
let timer = SwiftSignalKit.Timer(timeout: 1.0 * 60.0 * 60.0, repeat: false, completion: { [weak self] in
|
||
self?.storedPassword = nil
|
||
}, queue: Queue.mainQueue())
|
||
self.storedPassword = (password, CFAbsoluteTimeGetCurrent(), timer)
|
||
timer.start()
|
||
}
|
||
|
||
public func getStoredSecureIdPassword() -> String? {
|
||
if let (password, timestamp, timer) = self.storedPassword {
|
||
if CFAbsoluteTimeGetCurrent() > timestamp + 1.0 * 60.0 * 60.0 {
|
||
timer.invalidate()
|
||
self.storedPassword = nil
|
||
}
|
||
return password
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
public func chatLocationInput(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> ChatLocationInput {
|
||
switch location {
|
||
case let .peer(peerId):
|
||
return .peer(peerId: peerId, threadId: nil)
|
||
case let .replyThread(data):
|
||
if data.isForumPost || data.peerId.namespace != Namespaces.Peer.CloudChannel {
|
||
return .peer(peerId: data.peerId, threadId: data.threadId)
|
||
} else {
|
||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||
return .thread(peerId: data.peerId, threadId: data.threadId, data: context.state)
|
||
}
|
||
case .customChatContents:
|
||
preconditionFailure()
|
||
}
|
||
}
|
||
|
||
public func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<MessageId?, NoError> {
|
||
switch location {
|
||
case .peer:
|
||
return .single(nil)
|
||
case let .replyThread(data):
|
||
if data.isForumPost, let peerId = location.peerId {
|
||
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.peerId, threadId: data.threadId)
|
||
return self.account.postbox.combinedView(keys: [viewKey])
|
||
|> map { views -> MessageId? in
|
||
if let threadInfo = views.views[viewKey] as? MessageHistoryThreadInfoView, let data = threadInfo.info?.data.get(MessageHistoryThreadData.self) {
|
||
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: data.maxOutgoingReadId)
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
} else if data.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||
return context.maxReadOutgoingMessageId
|
||
} else {
|
||
return .single(nil)
|
||
}
|
||
case .customChatContents:
|
||
return .single(nil)
|
||
}
|
||
}
|
||
|
||
public func chatLocationUnreadCount(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> Signal<Int, NoError> {
|
||
switch location {
|
||
case let .peer(peerId):
|
||
let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(id: peerId, handleThreads: false), .total(nil)])
|
||
return self.account.postbox.combinedView(keys: [unreadCountsKey])
|
||
|> map { views in
|
||
var unreadCount: Int32 = 0
|
||
|
||
if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView {
|
||
if let count = view.count(for: .peer(id: peerId, handleThreads: false)) {
|
||
unreadCount = count
|
||
}
|
||
}
|
||
|
||
return Int(unreadCount)
|
||
}
|
||
case let .replyThread(data):
|
||
if data.isForumPost {
|
||
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: data.peerId, threadId: data.threadId)
|
||
return self.account.postbox.combinedView(keys: [viewKey])
|
||
|> map { views -> Int in
|
||
if let threadInfo = views.views[viewKey] as? MessageHistoryThreadInfoView, let data = threadInfo.info?.data.get(MessageHistoryThreadData.self) {
|
||
return Int(data.incomingUnreadCount)
|
||
} else {
|
||
return 0
|
||
}
|
||
}
|
||
} else if data.peerId.namespace != Namespaces.Peer.CloudChannel {
|
||
return .single(0)
|
||
} else {
|
||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||
return context.unreadCount
|
||
}
|
||
case .customChatContents:
|
||
return .single(0)
|
||
}
|
||
}
|
||
|
||
public func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex) {
|
||
switch location {
|
||
case .peer:
|
||
let _ = self.engine.messages.applyMaxReadIndexInteractively(index: messageIndex).start()
|
||
case let .replyThread(data):
|
||
let context = chatLocationContext(holder: contextHolder, account: self.account, data: data)
|
||
context.applyMaxReadIndex(messageIndex: messageIndex)
|
||
case .customChatContents:
|
||
break
|
||
}
|
||
}
|
||
|
||
public func scheduleGroupCall(peerId: PeerId, parentController: ViewController) {
|
||
let _ = self.sharedContext.callManager?.scheduleGroupCall(context: self, peerId: peerId, endCurrentIfAny: true, parentController: parentController)
|
||
}
|
||
|
||
public func joinGroupCall(peerId: PeerId, invite: String?, requestJoinAsPeerId: ((@escaping (PeerId?) -> Void) -> Void)?, activeCall: EngineGroupCallDescription) {
|
||
let callResult = self.sharedContext.callManager?.joinGroupCall(context: self, peerId: peerId, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: false)
|
||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||
if currentPeerId == peerId {
|
||
self.sharedContext.navigateToCurrentCall()
|
||
} else {
|
||
let dataInput: Signal<(EnginePeer?, EnginePeer?), NoError>
|
||
if let currentPeerId = currentPeerId {
|
||
dataInput = self.engine.data.get(
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: currentPeerId)
|
||
)
|
||
} else {
|
||
dataInput = self.engine.data.get(
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||
)
|
||
|> map { peer -> (EnginePeer?, EnginePeer?) in
|
||
return (peer, nil)
|
||
}
|
||
}
|
||
|
||
let _ = (dataInput
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer, current in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard let peer = peer else {
|
||
return
|
||
}
|
||
let presentationData = strongSelf.sharedContext.currentPresentationData.with { $0 }
|
||
if let current = current {
|
||
switch current {
|
||
case .channel, .legacyGroup:
|
||
let title: String
|
||
let text: String
|
||
if case let .channel(channel) = current, case .broadcast = channel.info {
|
||
title = presentationData.strings.Call_LiveStreamInProgressTitle
|
||
text = presentationData.strings.Call_LiveStreamInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
} else {
|
||
title = presentationData.strings.Call_VoiceChatInProgressTitle
|
||
text = presentationData.strings.Call_VoiceChatInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
}
|
||
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||
})]), on: .root)
|
||
default:
|
||
let text: String
|
||
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||
text = presentationData.strings.Call_CallInProgressLiveStreamMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
} else {
|
||
text = presentationData.strings.Call_CallInProgressVoiceChatMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
}
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.sharedContext.callManager?.joinGroupCall(context: strongSelf, peerId: peer.id, invite: invite, requestJoinAsPeerId: requestJoinAsPeerId, initialCall: activeCall, endCurrentIfAny: true)
|
||
})]), on: .root)
|
||
}
|
||
} else {
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
})]), on: .root)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
public func requestCall(peerId: PeerId, isVideo: Bool, completion: @escaping () -> Void) {
|
||
// MARK: Swiftgram
|
||
let makeCall = {
|
||
guard let callResult = self.sharedContext.callManager?.requestCall(context: self, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false) else {
|
||
return
|
||
}
|
||
|
||
if case let .alreadyInProgress(currentPeerId) = callResult {
|
||
if currentPeerId == peerId {
|
||
completion()
|
||
self.sharedContext.navigateToCurrentCall()
|
||
} else {
|
||
let dataInput: Signal<(EnginePeer?, EnginePeer?), NoError>
|
||
if let currentPeerId = currentPeerId {
|
||
dataInput = self.engine.data.get(
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: currentPeerId)
|
||
)
|
||
} else {
|
||
dataInput = self.engine.data.get(
|
||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||
)
|
||
|> map { peer -> (EnginePeer?, EnginePeer?) in
|
||
return (peer, nil)
|
||
}
|
||
}
|
||
|
||
let _ = (dataInput
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer, current in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard let peer = peer else {
|
||
return
|
||
}
|
||
let presentationData = strongSelf.sharedContext.currentPresentationData.with { $0 }
|
||
if let current = current {
|
||
switch current {
|
||
case .channel, .legacyGroup:
|
||
let text: String
|
||
if case let .channel(channel) = current, case .broadcast = channel.info {
|
||
text = presentationData.strings.Call_LiveStreamInProgressCallMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
} else {
|
||
text = presentationData.strings.Call_VoiceChatInProgressCallMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string
|
||
}
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_VoiceChatInProgressTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.sharedContext.callManager?.requestCall(context: strongSelf, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||
completion()
|
||
})]), on: .root)
|
||
default:
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.sharedContext.callManager?.requestCall(context: strongSelf, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||
completion()
|
||
})]), on: .root)
|
||
}
|
||
} else if let strongSelf = self {
|
||
strongSelf.sharedContext.mainWindow?.present(textAlertController(context: strongSelf, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||
})]), on: .root)
|
||
}
|
||
})
|
||
}
|
||
} else {
|
||
completion()
|
||
}
|
||
// MARK: Swiftgram
|
||
}
|
||
if SGSimpleSettings.shared.confirmCalls {
|
||
let presentationData = self.sharedContext.currentPresentationData.with { $0 }
|
||
self.sharedContext.mainWindow?.present(textAlertController(context: self, title: nil, text: isVideo ? i18n("CallConfirmation.Video.Title", presentationData.strings.baseLanguageCode) : i18n("CallConfirmation.Audio.Title", presentationData.strings.baseLanguageCode), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Yes, action: { [weak self] in
|
||
guard let _ = self else {
|
||
return
|
||
}
|
||
makeCall()
|
||
})]), on: .root)
|
||
} else {
|
||
makeCall()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func chatLocationContext(holder: Atomic<ChatLocationContextHolder?>, account: Account, data: ChatReplyThreadMessage) -> ReplyThreadHistoryContext {
|
||
let holder = holder.modify { current in
|
||
if let current = current as? ChatLocationReplyContextHolderImpl {
|
||
return current
|
||
} else {
|
||
return ChatLocationReplyContextHolderImpl(account: account, data: data)
|
||
}
|
||
} as! ChatLocationReplyContextHolderImpl
|
||
return holder.context
|
||
}
|
||
|
||
private final class ChatLocationReplyContextHolderImpl: ChatLocationContextHolder {
|
||
let context: ReplyThreadHistoryContext
|
||
|
||
init(account: Account, data: ChatReplyThreadMessage) {
|
||
self.context = ReplyThreadHistoryContext(account: account, peerId: data.peerId, data: data)
|
||
}
|
||
}
|
||
|
||
func getAppConfiguration(postbox: Postbox) -> Signal<AppConfiguration, NoError> {
|
||
return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||
|> map { view -> AppConfiguration in
|
||
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
|
||
return appConfiguration
|
||
}
|
||
|> distinctUntilChanged
|
||
}
|
||
|
||
private func loadCountryCodes() -> [Country] {
|
||
guard let filePath = getAppBundle().path(forResource: "PhoneCountries", ofType: "txt") else {
|
||
return []
|
||
}
|
||
guard let stringData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||
return []
|
||
}
|
||
guard let data = String(data: stringData, encoding: .utf8) else {
|
||
return []
|
||
}
|
||
|
||
let delimiter = ";"
|
||
let endOfLine = "\n"
|
||
|
||
var result: [Country] = []
|
||
// var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:]
|
||
|
||
var currentLocation = data.startIndex
|
||
|
||
let locale = Locale(identifier: "en-US")
|
||
|
||
while true {
|
||
guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex) else {
|
||
break
|
||
}
|
||
|
||
let countryCode = String(data[currentLocation ..< codeRange.lowerBound])
|
||
|
||
guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else {
|
||
break
|
||
}
|
||
|
||
let countryId = String(data[codeRange.upperBound ..< idRange.lowerBound])
|
||
|
||
guard let patternRange = data.range(of: delimiter, options: [], range: idRange.upperBound ..< data.endIndex) else {
|
||
break
|
||
}
|
||
|
||
let pattern = String(data[idRange.upperBound ..< patternRange.lowerBound])
|
||
|
||
let maybeNameRange = data.range(of: endOfLine, options: [], range: patternRange.upperBound ..< data.endIndex)
|
||
|
||
let countryName = locale.localizedString(forIdentifier: countryId) ?? ""
|
||
if let _ = Int(countryCode) {
|
||
let code = Country.CountryCode(code: countryCode, prefixes: [], patterns: !pattern.isEmpty ? [pattern] : [])
|
||
let country = Country(id: countryId, name: countryName, localizedName: nil, countryCodes: [code], hidden: false)
|
||
result.append(country)
|
||
// countriesByPrefix["\(code.code)"] = (country, code)
|
||
}
|
||
|
||
if let maybeNameRange = maybeNameRange {
|
||
currentLocation = maybeNameRange.upperBound
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|