From 9cfdc274432a2004856d0332a78dbd1883e2cffe Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 27 Mar 2024 19:00:12 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AccountContext.swift | 2 + .../Sources/ItemListPeerItem.swift | 2 +- .../Postbox/Sources/PreferencesTable.swift | 11 + .../Postbox/Sources/PreferencesView.swift | 63 +++ submodules/Postbox/Sources/Views.swift | 11 + .../DataPrivacySettingsController.swift | 378 ++++++++++-------- .../PrivacyAndSecurityController.swift | 19 +- .../SelectivePrivacySettingsController.swift | 2 +- ...ectivePrivacySettingsPeersController.swift | 2 +- .../SyncCore/SyncCore_Namespaces.swift | 16 + .../TelegramEngine/Data/PeersData.swift | 4 +- .../TelegramEngine/Messages/BotWebView.swift | 39 +- .../Peers/TelegramEnginePeers.swift | 6 +- .../TelegramNotices/Sources/Notices.swift | 31 ++ .../Sources/PresenceStrings.swift | 25 +- submodules/TelegramUI/BUILD | 1 + .../PeerInfoScreenBusinessHoursItem.swift | 8 + .../PeerInfoScreen/Sources/PeerInfoData.swift | 43 +- .../Sources/PeerInfoScreen.swift | 58 ++- .../Settings/BotSettingsScreen/BUILD | 25 ++ .../Sources/BotListSettingsScreen.swift | 172 ++++++++ .../Sources/BotSettingsScreen.swift | 152 +++++++ .../Sources/PeerSelectionScreen.swift | 34 +- .../TelegramUI/Sources/ChatController.swift | 2 - ...llerDisplayBusinessBotMessageTooltip.swift | 63 +++ .../Sources/ChatHistoryListNode.swift | 38 ++ .../Sources/ChatMessageTransitionNode.swift | 49 ++- .../Sources/SharedAccountContext.swift | 9 + .../WebUI/Sources/WebAppController.swift | 59 ++- 30 files changed, 1089 insertions(+), 237 deletions(-) create mode 100644 submodules/TelegramUI/Components/Settings/BotSettingsScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotListSettingsScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotSettingsScreen.swift create mode 100644 submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index fa815b93cc..2b035e47c0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5b80bbbf1f..4b9ab42500 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -977,6 +977,8 @@ public protocol SharedAccountContext: AnyObject { func makeBusinessLinksSetupScreenInitialData(context: AccountContext) -> Signal func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal + 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 diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index f0b0e44e08..c9eadd682a 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -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, diff --git a/submodules/Postbox/Sources/PreferencesTable.swift b/submodules/Postbox/Sources/PreferencesTable.swift index e76c862cee..68fb95543d 100644 --- a/submodules/Postbox/Sources/PreferencesTable.swift +++ b/submodules/Postbox/Sources/PreferencesTable.swift @@ -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) diff --git a/submodules/Postbox/Sources/PreferencesView.swift b/submodules/Postbox/Sources/PreferencesView.swift index d739bb97a4..8cb9d078f4 100644 --- a/submodules/Postbox/Sources/PreferencesView.swift +++ b/submodules/Postbox/Sources/PreferencesView.swift @@ -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 + } +} diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index c3b0e95f18..48dd9e5633 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -53,6 +53,7 @@ public enum PostboxViewKey: Hashable { case peerChatState(peerId: PeerId) case orderedItemList(id: Int32) case preferences(keys: Set) + 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): diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index f5ad980054..09624a343a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -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 } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 5103da8ba8..558e78a84a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -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)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 724388d781..7a8010d034 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -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) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index ccc92f6b50..59ff6b2bfa 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 7ae701faa7..ce131e8859 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 25530f9d0b..7f70cd4cdd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -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 } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index cd762c591d..380047db06 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -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 { +func _internal_updateBotBiometricsState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotBiometricsState?) -> TelegramBotBiometricsState) -> Signal { 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, NoError> { + let viewKey: PostboxViewKey = PostboxViewKey.preferencesPrefix(keyPrefix: PreferencesKeys.botBiometricsStatePrefix()) + return account.postbox.combinedView(keys: [viewKey]) + |> map { views -> Set in + guard let view = views.views[viewKey] as? PreferencesPrefixView else { + return Set() + } + + var result = Set() + 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 { return account.postbox.transaction { transaction -> Bool in var isPaused = false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 9bbd612e7e..e78cbf32eb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -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, NoError> { + return _internal_botsWithBiometricState(account: self.account) + } + public func toggleChatManagingBotIsPaused(chatId: EnginePeer.Id) { let _ = _internal_toggleChatManagingBotIsPaused(account: self.account, chatId: chatId).startStandalone() } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 61d4156915..789ee34c52 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -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) -> Signal { + 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, count: Int = 1) -> Signal { + 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) + } + } } diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index 0f88a6c5c0..22db5b1017 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -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 { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 5c3a579dab..986ef44504 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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": [], diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index 9e43592add..87b007469a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -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, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 46f87af3bb..20bd0ca304 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -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 ) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 660f36a775..39e944de69 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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(false) private let nearbyPeerDistance: Int32? @@ -2472,6 +2482,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private let storiesReady = ValuePromise(true, ignoreRepeated: true) + private var personalChannelsDisposable: Disposable? + private let _ready = Promise() var ready: Promise { 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) { diff --git a/submodules/TelegramUI/Components/Settings/BotSettingsScreen/BUILD b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/BUILD new file mode 100644 index 0000000000..f561ffc45a --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/BUILD @@ -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", + ], +) + diff --git a/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotListSettingsScreen.swift b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotListSettingsScreen.swift new file mode 100644 index 0000000000..7f69a48bca --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotListSettingsScreen.swift @@ -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 +} diff --git a/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotSettingsScreen.swift b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotSettingsScreen.swift new file mode 100644 index 0000000000..d27ef9b873 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BotSettingsScreen/Sources/BotSettingsScreen.swift @@ -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 +} diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index 8c1900de34..3e4bc6dab7 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -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 { + public static func initialData(context: AccountContext, channels: [TelegramAdminedPublicChannel]?) -> Signal { 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) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bdb2f8ceef..26e17e3558 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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? diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift new file mode 100644 index 0000000000..8849973e90 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift @@ -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() + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 2b4e865fba..835d9937f0 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -708,6 +708,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var frozenMessageForScrollingReset: EngineMessage.Id? + private var hasDisplayedBusinessBotMessageTooltip: Bool = false + private let _isReady = ValuePromise(false, ignoreRepeated: true) public var isReady: Signal { 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) + } + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 6033787e87..3e20915deb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index abfa71b331..fc6db8e202 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index eeeeb4784a..3300463e4d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -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 {