Various improvements

This commit is contained in:
Isaac 2024-03-27 19:00:12 +04:00
parent 120d23292d
commit 9cfdc27443
30 changed files with 1089 additions and 237 deletions

View File

@ -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";

View File

@ -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>

View File

@ -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,

View File

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

View File

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

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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 {

View File

@ -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": [],

View File

@ -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,

View File

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

View File

@ -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) {

View File

@ -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",
],
)

View File

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

View File

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

View File

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

View File

@ -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?

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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 {