mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Various improvements
This commit is contained in:
parent
120d23292d
commit
9cfdc27443
@ -11438,6 +11438,8 @@ Sorry for the inconvenience.";
|
||||
"PeerInfo.BusinessHours.StatusOpensOnDate" = "Opens %@";
|
||||
"PeerInfo.BusinessHours.StatusOpensTodayAt" = "Opens today at %@";
|
||||
"PeerInfo.BusinessHours.StatusOpensTomorrowAt" = "Opens tomorrow at %@";
|
||||
"PeerInfo.BusinessHours.StatusOpensInDays_1" = "Opens in 1 day";
|
||||
"PeerInfo.BusinessHours.StatusOpensInDays_any" = "Opens in %d days";
|
||||
"PeerInfo.BusinessHours.TimezoneSwitchMy" = "my time";
|
||||
"PeerInfo.BusinessHours.TimezoneSwitchBusiness" = "local time";
|
||||
"PeerInfo.BusinessHours.Label" = "business hours";
|
||||
|
@ -977,6 +977,8 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeBusinessLinksSetupScreenInitialData(context: AccountContext) -> Signal<BusinessLinksSetupScreenInitialData, NoError>
|
||||
func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController
|
||||
func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
|
||||
func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController
|
||||
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>
|
||||
|
@ -495,7 +495,7 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem {
|
||||
label: ItemListPeerItemLabel,
|
||||
editing: ItemListPeerItemEditing,
|
||||
revealOptions: ItemListPeerItemRevealOptions? = nil,
|
||||
switchValue: ItemListPeerItemSwitch?,
|
||||
switchValue: ItemListPeerItemSwitch? = nil,
|
||||
enabled: Bool,
|
||||
highlighted: Bool = false,
|
||||
selectable: Bool,
|
||||
|
@ -37,6 +37,17 @@ final class PreferencesTable: Table {
|
||||
}
|
||||
}
|
||||
|
||||
func getKeysWithPrefix(keyPrefix: ValueBoxKey) -> [ValueBoxKey] {
|
||||
var result: [ValueBoxKey] = []
|
||||
|
||||
self.valueBox.range(self.table, start: keyPrefix, end: keyPrefix.successor, keys: { key in
|
||||
result.append(key)
|
||||
return true
|
||||
}, limit: 100000)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func set(key: ValueBoxKey, value: PreferencesEntry?, operations: inout [PreferencesOperation]) {
|
||||
self.cachedEntries[key] = CachedEntry(entry: value)
|
||||
updatedEntryKeys.insert(key)
|
||||
|
@ -73,3 +73,66 @@ public final class PreferencesView: PostboxView {
|
||||
self.values = view.values
|
||||
}
|
||||
}
|
||||
|
||||
final class MutablePreferencesPrefixView: MutablePostboxView {
|
||||
fileprivate let keyPrefix: ValueBoxKey
|
||||
fileprivate var values: [ValueBoxKey: PreferencesEntry]
|
||||
|
||||
init(postbox: PostboxImpl, keyPrefix: ValueBoxKey) {
|
||||
self.keyPrefix = keyPrefix
|
||||
|
||||
var values: [ValueBoxKey: PreferencesEntry] = [:]
|
||||
for key in postbox.preferencesTable.getKeysWithPrefix(keyPrefix: keyPrefix) {
|
||||
if let value = postbox.preferencesTable.get(key: key) {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
self.values = values
|
||||
}
|
||||
|
||||
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
for operation in transaction.currentPreferencesOperations {
|
||||
switch operation {
|
||||
case let .update(key, value):
|
||||
if self.keyPrefix.isPrefix(to: key) {
|
||||
let currentValue = self.values[key]
|
||||
var updatedValue = false
|
||||
if let value = value, let currentValue = currentValue {
|
||||
if value != currentValue {
|
||||
updatedValue = true
|
||||
}
|
||||
} else if (value != nil) != (currentValue != nil) {
|
||||
updatedValue = true
|
||||
}
|
||||
if updatedValue {
|
||||
if let value = value {
|
||||
self.values[key] = value
|
||||
} else {
|
||||
self.values.removeValue(forKey: key)
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return PreferencesPrefixView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class PreferencesPrefixView: PostboxView {
|
||||
public let values: [ValueBoxKey: PreferencesEntry]
|
||||
|
||||
init(_ view: MutablePreferencesPrefixView) {
|
||||
self.values = view.values
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ public enum PostboxViewKey: Hashable {
|
||||
case peerChatState(peerId: PeerId)
|
||||
case orderedItemList(id: Int32)
|
||||
case preferences(keys: Set<ValueBoxKey>)
|
||||
case preferencesPrefix(keyPrefix: ValueBoxKey)
|
||||
case globalMessageTags(globalTag: GlobalMessageTags, position: MessageIndex, count: Int, groupingPredicate: ((Message, Message) -> Bool)?)
|
||||
case peer(peerId: PeerId, components: PeerViewComponents)
|
||||
case pendingMessageActions(type: PendingMessageActionType)
|
||||
@ -112,6 +113,8 @@ public enum PostboxViewKey: Hashable {
|
||||
hasher.combine(id)
|
||||
case .preferences:
|
||||
hasher.combine(3)
|
||||
case .preferencesPrefix:
|
||||
hasher.combine(21)
|
||||
case .globalMessageTags:
|
||||
hasher.combine(4)
|
||||
case let .peer(peerId, _):
|
||||
@ -260,6 +263,12 @@ public enum PostboxViewKey: Hashable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .preferencesPrefix(lhsKeyPrefix):
|
||||
if case let .preferencesPrefix(rhsKeyPrefix) = rhs, lhsKeyPrefix == rhsKeyPrefix {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .globalMessageTags(globalTag, position, count, _):
|
||||
if case .globalMessageTags(globalTag, position, count, _) = rhs {
|
||||
return true
|
||||
@ -542,6 +551,8 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
|
||||
return MutableOrderedItemListView(postbox: postbox, collectionId: id)
|
||||
case let .preferences(keys):
|
||||
return MutablePreferencesView(postbox: postbox, keys: keys)
|
||||
case let .preferencesPrefix(keyPrefix):
|
||||
return MutablePreferencesPrefixView(postbox: postbox, keyPrefix: keyPrefix)
|
||||
case let .globalMessageTags(globalTag, position, count, groupingPredicate):
|
||||
return MutableGlobalMessageTagsView(postbox: postbox, globalTag: globalTag, position: position, count: count, groupingPredicate: groupingPredicate)
|
||||
case let .peer(peerId, components):
|
||||
|
@ -22,8 +22,9 @@ private final class DataPrivacyControllerArguments {
|
||||
let updateSyncContacts: (Bool) -> Void
|
||||
let updateSuggestFrequentContacts: (Bool) -> Void
|
||||
let deleteCloudDrafts: () -> Void
|
||||
let openBotListSettings: () -> Void
|
||||
|
||||
init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void) {
|
||||
init(account: Account, clearPaymentInfo: @escaping () -> Void, updateSecretChatLinkPreviews: @escaping (Bool) -> Void, deleteContacts: @escaping () -> Void, updateSyncContacts: @escaping (Bool) -> Void, updateSuggestFrequentContacts: @escaping (Bool) -> Void, deleteCloudDrafts: @escaping () -> Void, openBotListSettings: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.clearPaymentInfo = clearPaymentInfo
|
||||
self.updateSecretChatLinkPreviews = updateSecretChatLinkPreviews
|
||||
@ -31,6 +32,7 @@ private final class DataPrivacyControllerArguments {
|
||||
self.updateSyncContacts = updateSyncContacts
|
||||
self.updateSuggestFrequentContacts = updateSuggestFrequentContacts
|
||||
self.deleteCloudDrafts = deleteCloudDrafts
|
||||
self.openBotListSettings = openBotListSettings
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +42,7 @@ private enum PrivacyAndSecuritySection: Int32 {
|
||||
case chats
|
||||
case payments
|
||||
case secretChats
|
||||
case bots
|
||||
}
|
||||
|
||||
private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
@ -62,144 +65,157 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
case secretChatLinkPreviews(PresentationTheme, String, Bool)
|
||||
case secretChatLinkPreviewsInfo(PresentationTheme, String)
|
||||
|
||||
case botList
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
|
||||
return PrivacyAndSecuritySection.contacts.rawValue
|
||||
case .frequentContacts, .frequentContactsInfo:
|
||||
return PrivacyAndSecuritySection.frequentContacts.rawValue
|
||||
case .chatsHeader, .deleteCloudDrafts:
|
||||
return PrivacyAndSecuritySection.chats.rawValue
|
||||
case .paymentHeader, .clearPaymentInfo, .paymentInfo:
|
||||
return PrivacyAndSecuritySection.payments.rawValue
|
||||
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
|
||||
return PrivacyAndSecuritySection.secretChats.rawValue
|
||||
case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo:
|
||||
return PrivacyAndSecuritySection.contacts.rawValue
|
||||
case .frequentContacts, .frequentContactsInfo:
|
||||
return PrivacyAndSecuritySection.frequentContacts.rawValue
|
||||
case .chatsHeader, .deleteCloudDrafts:
|
||||
return PrivacyAndSecuritySection.chats.rawValue
|
||||
case .paymentHeader, .clearPaymentInfo, .paymentInfo:
|
||||
return PrivacyAndSecuritySection.payments.rawValue
|
||||
case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo:
|
||||
return PrivacyAndSecuritySection.secretChats.rawValue
|
||||
case .botList:
|
||||
return PrivacyAndSecuritySection.bots.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .contactsHeader:
|
||||
return 0
|
||||
case .deleteContacts:
|
||||
return 1
|
||||
case .syncContacts:
|
||||
return 2
|
||||
case .syncContactsInfo:
|
||||
return 3
|
||||
case .contactsHeader:
|
||||
return 0
|
||||
case .deleteContacts:
|
||||
return 1
|
||||
case .syncContacts:
|
||||
return 2
|
||||
case .syncContactsInfo:
|
||||
return 3
|
||||
|
||||
case .frequentContacts:
|
||||
return 4
|
||||
case .frequentContactsInfo:
|
||||
return 5
|
||||
case .frequentContacts:
|
||||
return 4
|
||||
case .frequentContactsInfo:
|
||||
return 5
|
||||
|
||||
case .chatsHeader:
|
||||
return 6
|
||||
case .deleteCloudDrafts:
|
||||
return 7
|
||||
case .chatsHeader:
|
||||
return 6
|
||||
case .deleteCloudDrafts:
|
||||
return 7
|
||||
|
||||
case .paymentHeader:
|
||||
return 8
|
||||
case .clearPaymentInfo:
|
||||
return 9
|
||||
case .paymentInfo:
|
||||
return 10
|
||||
case .paymentHeader:
|
||||
return 8
|
||||
case .clearPaymentInfo:
|
||||
return 9
|
||||
case .paymentInfo:
|
||||
return 10
|
||||
|
||||
case .secretChatLinkPreviewsHeader:
|
||||
return 11
|
||||
case .secretChatLinkPreviews:
|
||||
return 12
|
||||
case .secretChatLinkPreviewsInfo:
|
||||
return 13
|
||||
case .secretChatLinkPreviewsHeader:
|
||||
return 11
|
||||
case .secretChatLinkPreviews:
|
||||
return 12
|
||||
case .secretChatLinkPreviewsInfo:
|
||||
return 13
|
||||
|
||||
case .botList:
|
||||
return 14
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .contactsHeader(lhsTheme, lhsText):
|
||||
if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deleteContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .syncContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .syncContactsInfo(lhsTheme, lhsText):
|
||||
if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .frequentContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .frequentContactsInfo(lhsTheme, lhsText):
|
||||
if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .chatsHeader(lhsTheme, lhsText):
|
||||
if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deleteCloudDrafts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .deleteCloudDrafts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .paymentHeader(lhsTheme, lhsText):
|
||||
if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .paymentInfo(lhsTheme, lhsText):
|
||||
if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText):
|
||||
if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText):
|
||||
if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .contactsHeader(lhsTheme, lhsText):
|
||||
if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deleteContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .deleteContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .syncContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .syncContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .syncContactsInfo(lhsTheme, lhsText):
|
||||
if case let .syncContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .frequentContacts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .frequentContacts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .frequentContactsInfo(lhsTheme, lhsText):
|
||||
if case let .frequentContactsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .chatsHeader(lhsTheme, lhsText):
|
||||
if case let .chatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deleteCloudDrafts(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .deleteCloudDrafts(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .paymentHeader(lhsTheme, lhsText):
|
||||
if case let .paymentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .clearPaymentInfo(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .clearPaymentInfo(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .paymentInfo(lhsTheme, lhsText):
|
||||
if case let .paymentInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviewsHeader(lhsTheme, lhsText):
|
||||
if case let .secretChatLinkPreviewsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviews(lhsTheme, lhsText, lhsEnabled):
|
||||
if case let .secretChatLinkPreviews(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .secretChatLinkPreviewsInfo(lhsTheme, lhsText):
|
||||
if case let .secretChatLinkPreviewsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .botList:
|
||||
if case .botList = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,46 +226,58 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! DataPrivacyControllerArguments
|
||||
switch self {
|
||||
case let .contactsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .deleteContacts(_, text, value):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.deleteContacts()
|
||||
})
|
||||
case let .syncContacts(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSyncContacts(updatedValue)
|
||||
})
|
||||
case let .syncContactsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .frequentContacts(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSuggestFrequentContacts(updatedValue)
|
||||
})
|
||||
case let .frequentContactsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .chatsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .deleteCloudDrafts(_, text, value):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.deleteCloudDrafts()
|
||||
})
|
||||
case let .paymentHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .clearPaymentInfo(_, text, enabled):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.clearPaymentInfo()
|
||||
})
|
||||
case let .paymentInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .secretChatLinkPreviewsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .secretChatLinkPreviews(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSecretChatLinkPreviews(updatedValue)
|
||||
})
|
||||
case let .secretChatLinkPreviewsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .contactsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .deleteContacts(_, text, value):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.deleteContacts()
|
||||
})
|
||||
case let .syncContacts(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSyncContacts(updatedValue)
|
||||
})
|
||||
case let .syncContactsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .frequentContacts(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSuggestFrequentContacts(updatedValue)
|
||||
})
|
||||
case let .frequentContactsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .chatsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .deleteCloudDrafts(_, text, value):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.deleteCloudDrafts()
|
||||
})
|
||||
case let .paymentHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .clearPaymentInfo(_, text, enabled):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.clearPaymentInfo()
|
||||
})
|
||||
case let .paymentInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .secretChatLinkPreviewsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .secretChatLinkPreviews(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
|
||||
arguments.updateSecretChatLinkPreviews(updatedValue)
|
||||
})
|
||||
case let .secretChatLinkPreviewsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case .botList:
|
||||
//TODO:localize
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: "Bot Settings",
|
||||
label: "",
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openBotListSettings()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,7 +289,7 @@ private struct DataPrivacyControllerState: Equatable {
|
||||
var deletingCloudDrafts: Bool = false
|
||||
}
|
||||
|
||||
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool) -> [PrivacyAndSecurityEntry] {
|
||||
private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [PrivacyAndSecurityEntry] {
|
||||
var entries: [PrivacyAndSecurityEntry] = []
|
||||
|
||||
entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle))
|
||||
@ -282,6 +310,10 @@ private func dataPrivacyControllerEntries(presentationData: PresentationData, st
|
||||
entries.append(.secretChatLinkPreviews(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviews, secretChatLinkPreviews ?? true))
|
||||
entries.append(.secretChatLinkPreviewsInfo(presentationData.theme, presentationData.strings.Privacy_SecretChatsLinkPreviewsHelp))
|
||||
|
||||
if hasBotSettings {
|
||||
entries.append(.botList)
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
@ -293,6 +325,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
}
|
||||
|
||||
var presentControllerImpl: ((ViewController) -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
@ -485,12 +518,20 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||||
])
|
||||
presentControllerImpl?(controller)
|
||||
}, openBotListSettings: {
|
||||
pushControllerImpl?(context.sharedContext.makeBotSettingsScreen(context: context, peerId: nil))
|
||||
})
|
||||
|
||||
actionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start())
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers())
|
||||
|> map { presentationData, state, noticeView, sharedData, preferences, recentPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let hasBotSettings = context.engine.peers.botsWithBiometricState()
|
||||
|> map { peerIds -> Bool in
|
||||
return !peerIds.isEmpty
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers(), hasBotSettings)
|
||||
|> map { presentationData, state, noticeView, sharedData, preferences, recentPeers, hasBotSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) })
|
||||
|
||||
let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
|
||||
@ -515,7 +556,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
|
||||
let animateChanges = false
|
||||
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers), style: .blocks, animateChanges: animateChanges)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, animateChanges: animateChanges)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
@ -527,6 +568,9 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
presentControllerImpl = { [weak controller] c in
|
||||
controller?.present(c, in: .window(.root))
|
||||
}
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
controller?.push(c)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ private enum PrivacyAndSecuritySection: Int32 {
|
||||
case account
|
||||
case messageAutoremove
|
||||
case dataSettings
|
||||
case loginEmail
|
||||
}
|
||||
|
||||
public enum PrivacyAndSecurityEntryTag: ItemListItemTag {
|
||||
@ -98,6 +99,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
case voiceCallPrivacy(PresentationTheme, String, String)
|
||||
case forwardPrivacy(PresentationTheme, String, String)
|
||||
case groupPrivacy(PresentationTheme, String, String)
|
||||
case groupPrivacyFooter
|
||||
case voiceMessagePrivacy(PresentationTheme, String, String, Bool)
|
||||
case messagePrivacy(PresentationTheme, Bool, Bool)
|
||||
case bioPrivacy(PresentationTheme, String, String)
|
||||
@ -121,9 +123,11 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .loginEmail, .loginEmailInfo, .messageAutoremoveTimeout, .messageAutoremoveInfo:
|
||||
case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .messageAutoremoveTimeout, .messageAutoremoveInfo:
|
||||
return PrivacyAndSecuritySection.general.rawValue
|
||||
case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .voiceCallPrivacy, .voiceMessagePrivacy, .messagePrivacy, .bioPrivacy, .birthdayPrivacy, .selectivePrivacyInfo:
|
||||
case .loginEmail, .loginEmailInfo:
|
||||
return PrivacyAndSecuritySection.loginEmail.rawValue
|
||||
case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .groupPrivacyFooter, .voiceCallPrivacy, .voiceMessagePrivacy, .messagePrivacy, .bioPrivacy, .birthdayPrivacy, .selectivePrivacyInfo:
|
||||
return PrivacyAndSecuritySection.privacy.rawValue
|
||||
case .autoArchiveHeader, .autoArchive, .autoArchiveInfo:
|
||||
return PrivacyAndSecuritySection.autoArchive.rawValue
|
||||
@ -174,6 +178,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
return 19
|
||||
case .groupPrivacy:
|
||||
return 20
|
||||
case .groupPrivacyFooter:
|
||||
return 21
|
||||
case .selectivePrivacyInfo:
|
||||
return 22
|
||||
case .autoArchiveHeader:
|
||||
@ -239,6 +245,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .groupPrivacyFooter:
|
||||
if case .groupPrivacyFooter = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .voiceCallPrivacy(lhsTheme, lhsText, lhsValue):
|
||||
if case let .voiceCallPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
@ -401,6 +413,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openGroupsPrivacy()
|
||||
})
|
||||
case .groupPrivacyFooter:
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown("You can restrict which users are allowed to add you to groups and channels."), sectionId: self.section)
|
||||
case let .voiceMessagePrivacy(theme, text, value, hasPremium):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, titleIcon: hasPremium ? PresentationResourcesItemList.premiumIcon(theme) : nil, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openVoiceMessagePrivacy()
|
||||
@ -637,6 +651,7 @@ private func privacyAndSecurityControllerEntries(
|
||||
}
|
||||
//TODO:localize
|
||||
entries.append(.groupPrivacy(presentationData.theme, "Invites", stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.groupInvitations)))
|
||||
entries.append(.groupPrivacyFooter)
|
||||
} else {
|
||||
entries.append(.phoneNumberPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_PhoneNumber, presentationData.strings.Channel_NotificationLoading))
|
||||
entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading))
|
||||
|
@ -1142,7 +1142,7 @@ public func selectivePrivacySettingsController(
|
||||
chatListFilters: nil,
|
||||
onlyUsers: false,
|
||||
disableChannels: true,
|
||||
disableBots: true
|
||||
disableBots: false
|
||||
)), options: [], filters: [.excludeSelf]))
|
||||
addPeerDisposable.set((controller.result
|
||||
|> take(1)
|
||||
|
@ -354,7 +354,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
|
||||
chatListFilters: nil,
|
||||
onlyUsers: false,
|
||||
disableChannels: true,
|
||||
disableBots: true
|
||||
disableBots: false
|
||||
)), options: [], alwaysEnabled: true))
|
||||
addPeerDisposable.set((controller.result
|
||||
|> take(1)
|
||||
|
@ -484,6 +484,22 @@ public struct PreferencesKeys {
|
||||
return key
|
||||
}
|
||||
|
||||
static func botBiometricsStatePrefix() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: PreferencesKeyValues.botBiometricsState.rawValue)
|
||||
return key
|
||||
}
|
||||
|
||||
static func extractBotBiometricsStatePeerId(key: ValueBoxKey) -> PeerId? {
|
||||
if key.length != 4 + 8 {
|
||||
return nil
|
||||
}
|
||||
if key.getInt32(0) != PreferencesKeyValues.botBiometricsState.rawValue {
|
||||
return nil
|
||||
}
|
||||
return PeerId(key.getInt64(4))
|
||||
}
|
||||
|
||||
public static func botBiometricsState(peerId: PeerId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 8)
|
||||
key.setInt32(0, value: PreferencesKeyValues.botBiometricsState.rawValue)
|
||||
|
@ -1734,7 +1734,7 @@ public extension TelegramEngine.EngineData.Item {
|
||||
}
|
||||
|
||||
public struct BotBiometricsState: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||
public typealias Result = TelegramBotBiometricsState
|
||||
public typealias Result = TelegramBotBiometricsState?
|
||||
|
||||
fileprivate var id: EnginePeer.Id
|
||||
public var mapKey: EnginePeer.Id {
|
||||
@ -1756,7 +1756,7 @@ public extension TelegramEngine.EngineData.Item {
|
||||
if let state = view.values[PreferencesKeys.botBiometricsState(peerId: self.id)]?.get(TelegramBotBiometricsState.self) {
|
||||
return state
|
||||
} else {
|
||||
return TelegramBotBiometricsState.default
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -331,34 +331,65 @@ public struct TelegramBotBiometricsState: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public var deviceId: Data
|
||||
public var accessRequested: Bool
|
||||
public var accessGranted: Bool
|
||||
public var opaqueToken: OpaqueToken?
|
||||
|
||||
public static var `default`: TelegramBotBiometricsState {
|
||||
public static func create() -> TelegramBotBiometricsState {
|
||||
var deviceId = Data(count: 32)
|
||||
deviceId.withUnsafeMutableBytes { buffer -> Void in
|
||||
arc4random_buf(buffer.assumingMemoryBound(to: UInt8.self).baseAddress!, buffer.count)
|
||||
}
|
||||
|
||||
return TelegramBotBiometricsState(
|
||||
deviceId: deviceId,
|
||||
accessRequested: false,
|
||||
accessGranted: false,
|
||||
opaqueToken: nil
|
||||
)
|
||||
}
|
||||
|
||||
public init(accessRequested: Bool, accessGranted: Bool, opaqueToken: OpaqueToken?) {
|
||||
public init(deviceId: Data, accessRequested: Bool, accessGranted: Bool, opaqueToken: OpaqueToken?) {
|
||||
self.deviceId = deviceId
|
||||
self.accessRequested = accessRequested
|
||||
self.accessGranted = accessGranted
|
||||
self.opaqueToken = opaqueToken
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) -> Signal<Never, NoError> {
|
||||
func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState?) -> TelegramBotBiometricsState) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Void in
|
||||
let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId))?.get(TelegramBotBiometricsState.self) ?? TelegramBotBiometricsState.default
|
||||
let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId))?.get(TelegramBotBiometricsState.self)
|
||||
|
||||
transaction.setPreferencesEntry(key: PreferencesKeys.botBiometricsState(peerId: peerId), value: PreferencesEntry(update(previousState)))
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
func _internal_botsWithBiometricState(account: Account) -> Signal<Set<EnginePeer.Id>, NoError> {
|
||||
let viewKey: PostboxViewKey = PostboxViewKey.preferencesPrefix(keyPrefix: PreferencesKeys.botBiometricsStatePrefix())
|
||||
return account.postbox.combinedView(keys: [viewKey])
|
||||
|> map { views -> Set<EnginePeer.Id> in
|
||||
guard let view = views.views[viewKey] as? PreferencesPrefixView else {
|
||||
return Set()
|
||||
}
|
||||
|
||||
var result = Set<EnginePeer.Id>()
|
||||
for (key, value) in view.values {
|
||||
guard let peerId = PreferencesKeys.extractBotBiometricsStatePeerId(key: key) else {
|
||||
continue
|
||||
}
|
||||
if value.get(TelegramBotBiometricsState.self) == nil {
|
||||
continue
|
||||
}
|
||||
result.insert(peerId)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_toggleChatManagingBotIsPaused(account: Account, chatId: EnginePeer.Id) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Bool in
|
||||
var isPaused = false
|
||||
|
@ -1512,10 +1512,14 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateBotBiometricsState(peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState) -> TelegramBotBiometricsState) {
|
||||
public func updateBotBiometricsState(peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState?) -> TelegramBotBiometricsState) {
|
||||
let _ = _internal_updateBotBiometricsState(account: self.account, peerId: peerId, update: update).startStandalone()
|
||||
}
|
||||
|
||||
public func botsWithBiometricState() -> Signal<Set<EnginePeer.Id>, NoError> {
|
||||
return _internal_botsWithBiometricState(account: self.account)
|
||||
}
|
||||
|
||||
public func toggleChatManagingBotIsPaused(chatId: EnginePeer.Id) {
|
||||
let _ = _internal_toggleChatManagingBotIsPaused(account: self.account, chatId: chatId).startStandalone()
|
||||
}
|
||||
|
@ -202,6 +202,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
||||
case dismissedBusinessBadge = 68
|
||||
case dismissedBirthdayPremiumGifts = 69
|
||||
case monetizationIntroDismissed = 70
|
||||
case businessBotMessageTooltip = 71
|
||||
|
||||
var key: ValueBoxKey {
|
||||
let v = ValueBoxKey(length: 4)
|
||||
@ -550,6 +551,9 @@ private struct ApplicationSpecificNoticeKeys {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.monetizationIntroDismissed.key)
|
||||
}
|
||||
|
||||
static func businessBotMessageTooltip() -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.businessBotMessageTooltip.key)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificNotice {
|
||||
@ -2318,4 +2322,31 @@ public struct ApplicationSpecificNotice {
|
||||
}
|
||||
|> take(1)
|
||||
}
|
||||
|
||||
public static func getBusinessBotMessageTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
|
||||
return accountManager.transaction { transaction -> Int32 in
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.businessBotMessageTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
return value.value
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func incrementBusinessBotMessageTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
|
||||
return accountManager.transaction { transaction -> Int in
|
||||
var currentValue: Int32 = 0
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.businessBotMessageTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
currentValue = value.value
|
||||
}
|
||||
let previousValue = currentValue
|
||||
currentValue += Int32(count)
|
||||
|
||||
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
|
||||
transaction.setNotice(ApplicationSpecificNoticeKeys.businessBotMessageTooltip(), entry)
|
||||
}
|
||||
|
||||
return Int(previousValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -209,15 +209,15 @@ public func stringForUserPresence(strings: PresentationStrings, day: RelativeTim
|
||||
private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32, format: HumanReadableStringFormat? = nil) -> PresentationStrings.FormattedString {
|
||||
let result: PresentationStrings.FormattedString
|
||||
switch day {
|
||||
case .today:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.todayFormatString(string) ?? strings.Time_TodayAt(string)
|
||||
case .yesterday:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.yesterdayFormatString(string) ?? strings.Time_YesterdayAt(string)
|
||||
case .tomorrow:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.tomorrowFormatString(string) ?? strings.Time_TomorrowAt(string)
|
||||
case .today:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.todayFormatString(string) ?? strings.Time_TodayAt(string)
|
||||
case .yesterday:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.yesterdayFormatString(string) ?? strings.Time_YesterdayAt(string)
|
||||
case .tomorrow:
|
||||
let string = stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)
|
||||
result = format?.tomorrowFormatString(string) ?? strings.Time_TomorrowAt(string)
|
||||
|
||||
}
|
||||
return result
|
||||
@ -228,17 +228,20 @@ public struct HumanReadableStringFormat {
|
||||
let tomorrowFormatString: (String) -> PresentationStrings.FormattedString
|
||||
let todayFormatString: (String) -> PresentationStrings.FormattedString
|
||||
let yesterdayFormatString: (String) -> PresentationStrings.FormattedString
|
||||
let daysFormatString: ((Int) -> PresentationStrings.FormattedString)?
|
||||
|
||||
public init(
|
||||
dateFormatString: @escaping (String) -> PresentationStrings.FormattedString,
|
||||
tomorrowFormatString: @escaping (String) -> PresentationStrings.FormattedString,
|
||||
todayFormatString: @escaping (String) -> PresentationStrings.FormattedString,
|
||||
yesterdayFormatString: @escaping (String) -> PresentationStrings.FormattedString = { PresentationStrings.FormattedString(string: $0, ranges: []) }
|
||||
yesterdayFormatString: @escaping (String) -> PresentationStrings.FormattedString = { PresentationStrings.FormattedString(string: $0, ranges: []) },
|
||||
daysFormatString: ((Int) -> PresentationStrings.FormattedString)? = nil
|
||||
) {
|
||||
self.dateFormatString = dateFormatString
|
||||
self.tomorrowFormatString = tomorrowFormatString
|
||||
self.todayFormatString = todayFormatString
|
||||
self.yesterdayFormatString = yesterdayFormatString
|
||||
self.daysFormatString = daysFormatString
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,6 +276,8 @@ public func humanReadableStringForTimestamp(strings: PresentationStrings, dateTi
|
||||
day = .tomorrow
|
||||
}
|
||||
return humanReadableStringForTimestamp(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, format: format)
|
||||
} else if dayDifference < 7, let daysFormatString = format?.daysFormatString {
|
||||
return daysFormatString(Int(dayDifference))
|
||||
} else {
|
||||
let string: String
|
||||
if alwaysShowTime {
|
||||
|
@ -445,6 +445,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController",
|
||||
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
|
||||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/BotSettingsScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -356,6 +356,9 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
|
||||
},
|
||||
yesterdayFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensTodayAt(value).string, ranges: [])
|
||||
},
|
||||
daysFormatString: { value in
|
||||
return PresentationStrings.FormattedString(string: presentationData.strings.PeerInfo_BusinessHours_StatusOpensInDays(Int32(value)), ranges: [])
|
||||
}
|
||||
)).string
|
||||
currentDayStatusText = dateText
|
||||
@ -429,6 +432,11 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
|
||||
}
|
||||
self.displayLocalTimezone = !self.displayLocalTimezone
|
||||
self.item?.requestLayout(false)
|
||||
|
||||
if !self.isExpanded {
|
||||
self.isExpanded = true
|
||||
self.item?.requestLayout(true)
|
||||
}
|
||||
},
|
||||
animateAlpha: true,
|
||||
animateScale: false,
|
||||
|
@ -35,6 +35,7 @@ final class PeerInfoState {
|
||||
let highlightedButton: PeerInfoHeaderButtonKey?
|
||||
let isEditingBirthDate: Bool
|
||||
let updatingBirthDate: TelegramBirthday??
|
||||
let personalChannels: [TelegramAdminedPublicChannel]?
|
||||
|
||||
init(
|
||||
isEditing: Bool,
|
||||
@ -44,7 +45,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: AvatarUploadProgress?,
|
||||
highlightedButton: PeerInfoHeaderButtonKey?,
|
||||
isEditingBirthDate: Bool,
|
||||
updatingBirthDate: TelegramBirthday??
|
||||
updatingBirthDate: TelegramBirthday??,
|
||||
personalChannels: [TelegramAdminedPublicChannel]?
|
||||
) {
|
||||
self.isEditing = isEditing
|
||||
self.selectedMessageIds = selectedMessageIds
|
||||
@ -54,6 +56,7 @@ final class PeerInfoState {
|
||||
self.highlightedButton = highlightedButton
|
||||
self.isEditingBirthDate = isEditingBirthDate
|
||||
self.updatingBirthDate = updatingBirthDate
|
||||
self.personalChannels = personalChannels
|
||||
}
|
||||
|
||||
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
|
||||
@ -65,7 +68,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -78,7 +82,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -91,7 +96,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -104,7 +110,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -117,7 +124,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -130,7 +138,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -143,7 +152,8 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
@ -156,7 +166,22 @@ final class PeerInfoState {
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: updatingBirthDate
|
||||
updatingBirthDate: updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
func withPersonalChannels(_ personalChannels: [TelegramAdminedPublicChannel]?) -> PeerInfoState {
|
||||
return PeerInfoState(
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: personalChannels
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1105,14 +1105,23 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
interaction.editingOpenNameColorSetup()
|
||||
}))
|
||||
|
||||
//TODO:localize
|
||||
var personalChannelTitle: String?
|
||||
if let personalChannel = data.personalChannel {
|
||||
personalChannelTitle = personalChannel.peer.compactDisplayTitle
|
||||
var displayPersonalChannel = false
|
||||
if data.personalChannel != nil {
|
||||
displayPersonalChannel = true
|
||||
} else if let personalChannels = state.personalChannels, !personalChannels.isEmpty {
|
||||
displayPersonalChannel = true
|
||||
}
|
||||
if displayPersonalChannel {
|
||||
//TODO:localize
|
||||
var personalChannelTitle: String?
|
||||
if let personalChannel = data.personalChannel {
|
||||
personalChannelTitle = personalChannel.peer.compactDisplayTitle
|
||||
}
|
||||
|
||||
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Channel", icon: nil, action: {
|
||||
interaction.editingOpenPersonalChannel()
|
||||
}))
|
||||
}
|
||||
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Channel", icon: nil, action: {
|
||||
interaction.editingOpenPersonalChannel()
|
||||
}))
|
||||
}
|
||||
|
||||
items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: {
|
||||
@ -2414,7 +2423,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
avatarUploadProgress: nil,
|
||||
highlightedButton: nil,
|
||||
isEditingBirthDate: false,
|
||||
updatingBirthDate: nil
|
||||
updatingBirthDate: nil,
|
||||
personalChannels: nil
|
||||
)
|
||||
private var forceIsContactPromise = ValuePromise<Bool>(false)
|
||||
private let nearbyPeerDistance: Int32?
|
||||
@ -2472,6 +2482,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
private let storiesReady = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||
|
||||
private var personalChannelsDisposable: Disposable?
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
@ -4464,6 +4476,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.updateAvatarDisposable.dispose()
|
||||
self.joinChannelDisposable.dispose()
|
||||
self.boostStatusDisposable?.dispose()
|
||||
self.personalChannelsDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -7685,7 +7698,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
|
||||
private func editingOpenPersonalChannel() {
|
||||
let _ = (PeerSelectionScreen.initialData(context: self.context)
|
||||
let _ = (PeerSelectionScreen.initialData(context: self.context, channels: self.state.personalChannels)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] initialData in
|
||||
guard let self else {
|
||||
return
|
||||
@ -11115,6 +11128,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshHasPersonalChannelsIfNeeded() {
|
||||
if !self.isSettings {
|
||||
return
|
||||
}
|
||||
if self.personalChannelsDisposable != nil {
|
||||
return
|
||||
}
|
||||
self.personalChannelsDisposable = (self.context.engine.peers.adminedPublicChannels(scope: .forPersonalProfile)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] personalChannels in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.personalChannelsDisposable?.dispose()
|
||||
self.personalChannelsDisposable = nil
|
||||
|
||||
if self.state.personalChannels != personalChannels {
|
||||
self.state = self.state.withPersonalChannels(personalChannels)
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
|
||||
@ -11736,6 +11774,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.refreshHasPersonalChannelsIfNeeded()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
|
@ -0,0 +1,25 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "BotSettingsScreen",
|
||||
module_name = "BotSettingsScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/ItemListPeerItem",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
@ -0,0 +1,172 @@
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import ItemListPeerItem
|
||||
import AccountContext
|
||||
|
||||
private final class BotListSettingsArguments {
|
||||
let context: AccountContext
|
||||
let openBot: (EnginePeer.Id) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
openBot: @escaping (EnginePeer.Id) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.openBot = openBot
|
||||
}
|
||||
}
|
||||
|
||||
private enum BotListSettingsSection: Int32 {
|
||||
case botItems
|
||||
}
|
||||
|
||||
private enum BotListSettingsEntry: ItemListNodeEntry {
|
||||
case botItem(peer: EnginePeer)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .botItem:
|
||||
return BotListSettingsSection.botItems.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: EnginePeer.Id {
|
||||
switch self {
|
||||
case let .botItem(peer):
|
||||
return peer.id
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: BotListSettingsEntry, rhs: BotListSettingsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .botItem(peer):
|
||||
if case .botItem(peer) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: BotListSettingsEntry, rhs: BotListSettingsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .botItem(lhsPeer):
|
||||
switch rhs {
|
||||
case let .botItem(rhsPeer):
|
||||
if lhsPeer.compactDisplayTitle != rhsPeer.compactDisplayTitle {
|
||||
return lhsPeer.compactDisplayTitle < rhsPeer.compactDisplayTitle
|
||||
}
|
||||
return lhsPeer.id < rhsPeer.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! BotListSettingsArguments
|
||||
switch self {
|
||||
case let .botItem(peer):
|
||||
return ItemListPeerItem(
|
||||
presentationData: presentationData,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
context: arguments.context,
|
||||
peer: peer,
|
||||
presence: nil,
|
||||
text: .none,
|
||||
label: .disclosure(""),
|
||||
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
enabled: true,
|
||||
selectable: true,
|
||||
sectionId: self.section,
|
||||
action: {
|
||||
arguments.openBot(peer.id)
|
||||
},
|
||||
setPeerIdWithRevealedOptions: { _, _ in
|
||||
},
|
||||
removePeer: { _ in
|
||||
},
|
||||
style: .blocks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BotListSettingsState: Equatable {
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private func botListSettingsEntries(
|
||||
presentationData: PresentationData,
|
||||
peers: [EnginePeer]
|
||||
) -> [BotListSettingsEntry] {
|
||||
var entries: [BotListSettingsEntry] = []
|
||||
|
||||
for peer in peers {
|
||||
entries.append(.botItem(peer: peer))
|
||||
}
|
||||
entries.sort(by: { $0 < $1 })
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func botListSettingsScreen(context: AccountContext) -> ViewController {
|
||||
let initialState = BotListSettingsState()
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((BotListSettingsState) -> BotListSettingsState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
let _ = updateState
|
||||
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let arguments = BotListSettingsArguments(
|
||||
context: context,
|
||||
openBot: { peerId in
|
||||
pushControllerImpl?(botSettingsScreen(context: context, peerId: peerId))
|
||||
}
|
||||
)
|
||||
|
||||
let botPeerList: Signal<[EnginePeer], NoError> = context.engine.peers.botsWithBiometricState()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
|
||||
return context.engine.data.subscribe(
|
||||
EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||
)
|
||||
|> map { peers -> [EnginePeer] in
|
||||
return peers.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get(),
|
||||
botPeerList
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, botPeerList -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
//TODO:localize
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Bots"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botListSettingsEntries(presentationData: presentationData, peers: botPeerList), style: .blocks, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
(controller?.navigationController as? NavigationController)?.pushViewController(c, animated: true)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
private final class BotSettingsArguments {
|
||||
let context: AccountContext
|
||||
let updateBiometryAccess: (Bool) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
updateBiometryAccess: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.updateBiometryAccess = updateBiometryAccess
|
||||
}
|
||||
}
|
||||
|
||||
private enum BotSettingsSection: Int32 {
|
||||
case settings
|
||||
}
|
||||
|
||||
private enum BotSettingsEntry: ItemListNodeEntry {
|
||||
case biometryAccess(value: Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .biometryAccess:
|
||||
return BotSettingsSection.settings.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int {
|
||||
switch self {
|
||||
case .biometryAccess:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: BotSettingsEntry, rhs: BotSettingsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .biometryAccess(value):
|
||||
if case .biometryAccess(value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: BotSettingsEntry, rhs: BotSettingsEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! BotSettingsArguments
|
||||
switch self {
|
||||
case let .biometryAccess(value):
|
||||
//TODO:localize
|
||||
return ItemListSwitchItem(
|
||||
presentationData: presentationData,
|
||||
title: "Biometry",
|
||||
value: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
updated: { value in
|
||||
arguments.updateBiometryAccess(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BotSettingsState: Equatable {
|
||||
init() {
|
||||
}
|
||||
}
|
||||
|
||||
private func botSettingsEntries(
|
||||
presentationData: PresentationData,
|
||||
peer: EnginePeer?,
|
||||
biometricsState: TelegramBotBiometricsState?
|
||||
) -> [BotSettingsEntry] {
|
||||
var entries: [BotSettingsEntry] = []
|
||||
|
||||
if let biometricsState {
|
||||
entries.append(.biometryAccess(value: biometricsState.accessGranted))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func botSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) -> ViewController {
|
||||
let initialState = BotSettingsState()
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((BotSettingsState) -> BotSettingsState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
let _ = pushControllerImpl
|
||||
let _ = updateState
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let arguments = BotSettingsArguments(
|
||||
context: context,
|
||||
updateBiometryAccess: { value in
|
||||
context.engine.peers.updateBotBiometricsState(peerId: peerId, update: { state in
|
||||
var state = state ?? TelegramBotBiometricsState.create()
|
||||
state.accessGranted = value
|
||||
return state
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
let data = context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: peerId)
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get(),
|
||||
data
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let (peer, biometricsState) = data
|
||||
|
||||
//TODO:localize
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(peer?.compactDisplayTitle ?? ""), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: botSettingsEntries(presentationData: presentationData, peer: peer, biometricsState: biometricsState), style: .blocks, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
(controller?.navigationController as? NavigationController)?.pushViewController(c)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
@ -410,18 +410,24 @@ final class PeerSelectionScreenComponent: Component {
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
self.channelsDisposable = (component.context.engine.peers.adminedPublicChannels(scope: .forPersonalProfile)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.channels = peers.map { peer in
|
||||
if let channels = component.initialData.channels, !channels.isEmpty {
|
||||
self.channels = channels.map { peer in
|
||||
return PeerSelectionScreen.ChannelInfo(peer: peer.peer, subscriberCount: peer.subscriberCount)
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.channelsDisposable = (component.context.engine.peers.adminedPublicChannels(scope: .forPersonalProfile)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.channels = peers.map { peer in
|
||||
return PeerSelectionScreen.ChannelInfo(peer: peer.peer, subscriberCount: peer.subscriberCount)
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
@ -687,9 +693,11 @@ final class PeerSelectionScreenComponent: Component {
|
||||
public final class PeerSelectionScreen: ViewControllerComponentContainer {
|
||||
public final class InitialData {
|
||||
public let channelId: EnginePeer.Id?
|
||||
public let channels: [TelegramAdminedPublicChannel]?
|
||||
|
||||
init(channelId: EnginePeer.Id?) {
|
||||
init(channelId: EnginePeer.Id?, channels: [TelegramAdminedPublicChannel]?) {
|
||||
self.channelId = channelId
|
||||
self.channels = channels
|
||||
}
|
||||
}
|
||||
|
||||
@ -737,7 +745,7 @@ public final class PeerSelectionScreen: ViewControllerComponentContainer {
|
||||
deinit {
|
||||
}
|
||||
|
||||
public static func initialData(context: AccountContext) -> Signal<InitialData, NoError> {
|
||||
public static func initialData(context: AccountContext, channels: [TelegramAdminedPublicChannel]?) -> Signal<InitialData, NoError> {
|
||||
return context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.PersonalChannel(id: context.account.peerId)
|
||||
)
|
||||
@ -746,7 +754,7 @@ public final class PeerSelectionScreen: ViewControllerComponentContainer {
|
||||
if case let .known(value) = personalChannel, let value {
|
||||
channelId = value.peerId
|
||||
}
|
||||
return InitialData(channelId: channelId)
|
||||
return InitialData(channelId: channelId, channels: channels)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,8 +455,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
weak var emojiPackTooltipController: TooltipScreen?
|
||||
weak var birthdayTooltipController: TooltipScreen?
|
||||
|
||||
var currentMessageTooltipScreens: [(TooltipScreen, ListViewItemNode)] = []
|
||||
|
||||
weak var slowmodeTooltipController: ChatSlowmodeHintController?
|
||||
|
||||
weak var currentContextController: ContextController?
|
||||
|
@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ChatMessageItemView
|
||||
import TelegramNotices
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func displayBusinessBotMessageTooltip(itemNode: ChatMessageItemView) {
|
||||
let _ = (ApplicationSpecificNotice.getBusinessBotMessageTooltip(accountManager: self.context.sharedContext.accountManager)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak itemNode] value in
|
||||
guard let self, let itemNode else {
|
||||
return
|
||||
}
|
||||
if value >= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
guard let statusNode = itemNode.getStatusNode() else {
|
||||
return
|
||||
}
|
||||
|
||||
let bounds = statusNode.view.convert(statusNode.view.bounds, to: self.chatDisplayNode.view)
|
||||
let location = CGPoint(x: bounds.midX, y: bounds.minY - 11.0)
|
||||
|
||||
//TODO:localize
|
||||
let tooltipController = TooltipController(content: .text("Only you can see that this message was sent by the bot."), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||||
self.checksTooltipController = tooltipController
|
||||
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
|
||||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController {
|
||||
strongSelf.checksTooltipController = nil
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.chatDisplayNode.messageTransitionNode.addCustomOffsetHandler(itemNode: itemNode, update: { [weak tooltipController] offset, transition in
|
||||
guard let tooltipController, tooltipController.isNodeLoaded else {
|
||||
return false
|
||||
}
|
||||
guard let containerView = tooltipController.view else {
|
||||
return false
|
||||
}
|
||||
containerView.bounds = containerView.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateOffsetAdditive(layer: containerView.layer, offset: offset)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return (self.chatDisplayNode, CGRect(origin: location, size: CGSize()))
|
||||
}))
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementBusinessBotMessageTooltip(accountManager: self.context.sharedContext.accountManager).startStandalone()
|
||||
})
|
||||
}
|
||||
}
|
@ -708,6 +708,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
var frozenMessageForScrollingReset: EngineMessage.Id?
|
||||
|
||||
private var hasDisplayedBusinessBotMessageTooltip: Bool = false
|
||||
|
||||
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
public var isReady: Signal<Bool, NoError> {
|
||||
return self._isReady.get()
|
||||
@ -2486,6 +2488,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
|
||||
var visibleAdOpaqueIds: [Data] = []
|
||||
var peerIdsWithRefreshStories: [PeerId] = []
|
||||
var visibleBusinessBotMessageId: EngineMessage.Id?
|
||||
|
||||
if indexRange.0 <= indexRange.1 {
|
||||
for i in (indexRange.0 ... indexRange.1) {
|
||||
@ -2679,6 +2682,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
hasAction = true
|
||||
}
|
||||
}
|
||||
if let _ = message.inlineBotAttribute {
|
||||
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
||||
if visibleBusinessBotMessageIdValue < message.id {
|
||||
visibleBusinessBotMessageId = message.id
|
||||
}
|
||||
} else {
|
||||
visibleBusinessBotMessageId = message.id
|
||||
}
|
||||
}
|
||||
if !hasAction {
|
||||
switch message.id.peerId.namespace {
|
||||
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
|
||||
@ -2695,6 +2707,15 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
hasAction = true
|
||||
}
|
||||
}
|
||||
if let _ = message.inlineBotAttribute {
|
||||
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
||||
if visibleBusinessBotMessageIdValue < message.id {
|
||||
visibleBusinessBotMessageId = message.id
|
||||
}
|
||||
} else {
|
||||
visibleBusinessBotMessageId = message.id
|
||||
}
|
||||
}
|
||||
if !hasAction {
|
||||
switch message.id.peerId.namespace {
|
||||
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
|
||||
@ -2873,6 +2894,23 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let visibleBusinessBotMessageId, !self.hasDisplayedBusinessBotMessageTooltip {
|
||||
var foundItemNode: ChatMessageItemView?
|
||||
self.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == visibleBusinessBotMessageId {
|
||||
foundItemNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
if let foundItemNode {
|
||||
self.hasDisplayedBusinessBotMessageTooltip = true
|
||||
|
||||
if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl {
|
||||
chatController.displayBusinessBotMessageTooltip(itemNode: foundItemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -289,6 +289,16 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset)
|
||||
}
|
||||
}
|
||||
|
||||
final class CustomOffsetHandlerImpl {
|
||||
weak var itemNode: ChatMessageItemView?
|
||||
let update: (CGFloat, ContainedViewLayoutTransition) -> Bool
|
||||
|
||||
init(itemNode: ChatMessageItemView, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) {
|
||||
self.itemNode = itemNode
|
||||
self.update = update
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatingItemNode: ASDisplayNode {
|
||||
let itemNode: ChatMessageItemView
|
||||
@ -900,6 +910,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
private var animatingItemNodes: [AnimatingItemNode] = []
|
||||
private var decorationItemNodes: [DecorationItemNodeImpl] = []
|
||||
private var messageReactionContexts: [MessageReactionContext] = []
|
||||
private var customOffsetHandlers: [CustomOffsetHandlerImpl] = []
|
||||
|
||||
var hasScheduledTransitions: Bool {
|
||||
return !self.currentPendingItems.isEmpty
|
||||
@ -958,12 +969,6 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
self.decorationItemNodes.append(decorationItemNode)
|
||||
self.addSubnode(decorationItemNode)
|
||||
|
||||
// let overlayController = OverlayTransitionContainerController()
|
||||
// overlayController.displayNode.isUserInteractionEnabled = false
|
||||
// overlayController.displayNode.addSubnode(decorationItemNode)
|
||||
// decorationItemNode.overlayController = overlayController
|
||||
// itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController)
|
||||
|
||||
return decorationItemNode
|
||||
}
|
||||
|
||||
@ -974,6 +979,20 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
decorationNode.overlayController?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
public func addCustomOffsetHandler(itemNode: ChatMessageItemView, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) -> Disposable {
|
||||
let handler = CustomOffsetHandlerImpl(itemNode: itemNode, update: update)
|
||||
self.customOffsetHandlers.append(handler)
|
||||
|
||||
return ActionDisposable { [weak self, weak handler] in
|
||||
Queue.mainQueue().async {
|
||||
guard let self, let handler else {
|
||||
return
|
||||
}
|
||||
self.customOffsetHandlers.removeAll(where: { $0 === handler })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func beginAnimation(itemNode: ChatMessageItemView, source: Source) {
|
||||
var contextSourceNode: ContextExtractedContentContainingNode?
|
||||
@ -1094,6 +1113,15 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
for decorationItemNode in self.decorationItemNodes {
|
||||
decorationItemNode.addExternalOffset(offset: offset, transition: transition)
|
||||
}
|
||||
var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = []
|
||||
for customOffsetHandler in self.customOffsetHandlers {
|
||||
if !customOffsetHandler.update(offset, transition) {
|
||||
removeCustomOffsetHandlers.append(customOffsetHandler)
|
||||
}
|
||||
}
|
||||
for customOffsetHandler in removeCustomOffsetHandlers {
|
||||
self.customOffsetHandlers.removeAll(where: { $0 === customOffsetHandler})
|
||||
}
|
||||
}
|
||||
for messageReactionContext in self.messageReactionContexts {
|
||||
messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode, isRotated: isRotated)
|
||||
@ -1108,6 +1136,15 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
||||
for decorationItemNode in self.decorationItemNodes {
|
||||
decorationItemNode.addContentOffset(offset: offset)
|
||||
}
|
||||
var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = []
|
||||
for customOffsetHandler in self.customOffsetHandlers {
|
||||
if !customOffsetHandler.update(offset, .immediate) {
|
||||
removeCustomOffsetHandlers.append(customOffsetHandler)
|
||||
}
|
||||
}
|
||||
for customOffsetHandler in removeCustomOffsetHandlers {
|
||||
self.customOffsetHandlers.removeAll(where: { $0 === customOffsetHandler})
|
||||
}
|
||||
}
|
||||
for messageReactionContext in self.messageReactionContexts {
|
||||
messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode)
|
||||
|
@ -61,6 +61,7 @@ import MediaEditor
|
||||
import MediaEditorScreen
|
||||
import BusinessIntroSetupScreen
|
||||
import TelegramNotices
|
||||
import BotSettingsScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -1962,6 +1963,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return CollectibleItemInfoScreen.initialData(context: context, peerId: peerId, subject: subject)
|
||||
}
|
||||
|
||||
public func makeBotSettingsScreen(context: AccountContext, peerId: EnginePeer.Id?) -> ViewController {
|
||||
if let peerId {
|
||||
return botSettingsScreen(context: context, peerId: peerId)
|
||||
} else {
|
||||
return botListSettingsScreen(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController {
|
||||
var modal = true
|
||||
let mappedSource: PremiumSource
|
||||
|
@ -1069,7 +1069,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
case "web_app_biometry_get_info":
|
||||
self.sendBiometryInfoReceivedEvent()
|
||||
case "web_app_biometry_request_access":
|
||||
self.requestBiometryAccess()
|
||||
var reason: String?
|
||||
if let json, let reasonValue = json["reason"] as? String, !reasonValue.isEmpty {
|
||||
reason = reasonValue
|
||||
}
|
||||
self.requestBiometryAccess(reason: reason)
|
||||
case "web_app_biometry_request_auth":
|
||||
self.requestBiometryAuth()
|
||||
case "web_app_biometry_update_token":
|
||||
@ -1078,6 +1082,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
tokenData = tokenDataValue.data(using: .utf8)
|
||||
}
|
||||
self.requestBiometryUpdateToken(tokenData: tokenData)
|
||||
case "web_app_biometry_open_settings":
|
||||
if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 {
|
||||
self.webView?.lastTouchTimestamp = nil
|
||||
|
||||
self.openBotSettings()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1411,6 +1421,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in
|
||||
let state = state ?? TelegramBotBiometricsState.create()
|
||||
return state
|
||||
})
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId)
|
||||
)
|
||||
@ -1418,6 +1433,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
|
||||
var data: [String: Any] = [:]
|
||||
if let biometricAuthentication = LocalAuth.biometricAuthentication {
|
||||
@ -1431,6 +1449,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
data["access_requested"] = state.accessRequested
|
||||
data["access_granted"] = state.accessGranted
|
||||
data["token_saved"] = state.opaqueToken != nil
|
||||
data["device_id"] = hexString(state.deviceId)
|
||||
} else {
|
||||
data["available"] = false
|
||||
}
|
||||
@ -1445,7 +1464,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func requestBiometryAccess() {
|
||||
fileprivate func requestBiometryAccess(reason: String?) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
@ -1453,12 +1472,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotBiometricsState(id: controller.botId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] botPeer, state in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] botPeer, currentState in
|
||||
guard let self, let botPeer, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
if state.accessRequested {
|
||||
if let currentState, currentState.accessRequested {
|
||||
self.sendBiometryInfoReceivedEvent()
|
||||
return
|
||||
}
|
||||
@ -1469,7 +1488,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
self.context.engine.peers.updateBotBiometricsState(peerId: botPeer.id, update: { state in
|
||||
var state = state
|
||||
var state = state ?? TelegramBotBiometricsState.create()
|
||||
|
||||
state.accessRequested = true
|
||||
state.accessGranted = granted
|
||||
return state
|
||||
@ -1479,8 +1499,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let alertText = "Do you want to allow \(botPeer.compactDisplayTitle) to use Face ID?"
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: alertText, actions: [
|
||||
var alertTitle: String?
|
||||
let alertText: String
|
||||
if let reason {
|
||||
alertTitle = "Do you want to allow \(botPeer.compactDisplayTitle) to use Face ID?"
|
||||
alertText = reason
|
||||
} else {
|
||||
alertText = "Do you want to allow \(botPeer.compactDisplayTitle) to use Face ID?"
|
||||
}
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: alertTitle, text: alertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: {
|
||||
updateAccessGranted(false)
|
||||
}),
|
||||
@ -1503,6 +1530,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
|
||||
if state.accessRequested && state.accessGranted {
|
||||
guard let controller = self.controller else {
|
||||
@ -1609,7 +1639,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
if let encryptedData {
|
||||
self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in
|
||||
var state = state
|
||||
var state = state ?? TelegramBotBiometricsState.create()
|
||||
state.opaqueToken = encryptedData
|
||||
return state
|
||||
})
|
||||
@ -1640,7 +1670,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}.start()
|
||||
} else {
|
||||
self.context.engine.peers.updateBotBiometricsState(peerId: controller.botId, update: { state in
|
||||
var state = state
|
||||
var state = state ?? TelegramBotBiometricsState.create()
|
||||
state.opaqueToken = nil
|
||||
return state
|
||||
})
|
||||
@ -1657,6 +1687,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func openBotSettings() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
if let navigationController = controller.getNavigationController() {
|
||||
let settingsController = self.context.sharedContext.makeBotSettingsScreen(context: self.context, peerId: controller.botId)
|
||||
settingsController.navigationPresentation = .modal
|
||||
navigationController.pushViewController(settingsController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var controllerNode: Node {
|
||||
|
Loading…
x
Reference in New Issue
Block a user