diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d4ada765e3..333768a1ac 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6873,7 +6873,7 @@ Sorry for the inconvenience."; "InviteLink.Create.RequestApprovalOnInfoChannel" = "New users will be able to join the channel only after having been approved by the admins."; "InviteLink.Create.LinkName" = "Link Name (Optional)"; -"InviteLink.Create.LinkNameInfo" = "You can provide an optional name for this link."; +"InviteLink.Create.LinkNameInfo" = "Only you and other admins will see this name."; "MemberRequests.Title" = "Member Requests"; "MemberRequests.DescriptionGroup" = "Some [additional links]() are set up to accept requests to join the group."; @@ -6962,3 +6962,19 @@ Sorry for the inconvenience."; "Time.AtDate" = "last seen %@"; "Stickers.ShowMore" = "Show More"; + +"Notifications.PrivateChats" = "Private Chats"; +"Notifications.GroupChats" = "Group Chats"; +"Notifications.Channels" = "Channels"; + +"Notifications.CategoryExceptions_0" = "%@ exceptions"; +"Notifications.CategoryExceptions_1" = "%@ exception"; +"Notifications.CategoryExceptions_2" = "%@ exceptions"; +"Notifications.CategoryExceptions_3_10" = "%@ exceptions"; +"Notifications.CategoryExceptions_many" = "%@ exceptions"; +"Notifications.CategoryExceptions_any" = "%@ exceptions"; + +"Notifications.DeleteAllExceptions" = "Delete All Exceptions"; + +"Notifications.On" = "On"; +"Notifications.Off" = "Off"; diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 1d14e64dff..a46b433791 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -328,7 +328,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode) } let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift index 2cb49c5ecb..ddcbd195f0 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift @@ -61,18 +61,8 @@ private final class NotificationsAndSoundsArguments { let authorizeNotifications: () -> Void let suppressWarning: () -> Void - let updateMessageAlerts: (Bool) -> Void - let updateMessagePreviews: (Bool) -> Void - let updateMessageSound: (PeerMessageSound) -> Void - - let updateGroupAlerts: (Bool) -> Void - let updateGroupPreviews: (Bool) -> Void - let updateGroupSound: (PeerMessageSound) -> Void - - let updateChannelAlerts: (Bool) -> Void - let updateChannelPreviews: (Bool) -> Void - let updateChannelSound: (PeerMessageSound) -> Void - + let openPeerCategory: (NotificationsPeerCategory) -> Void + let updateInAppSounds: (Bool) -> Void let updateInAppVibration: (Bool) -> Void let updateInAppPreviews: (Bool) -> Void @@ -84,29 +74,19 @@ private final class NotificationsAndSoundsArguments { let updateJoinedNotifications: (Bool) -> Void let resetNotifications: () -> Void - - let updatedExceptionMode: (NotificationExceptionMode) -> Void - + let openAppSettings: () -> Void let updateNotificationsFromAllAccounts: (Bool) -> Void - init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateChannelAlerts: @escaping (Bool) -> Void, updateChannelPreviews: @escaping (Bool) -> Void, updateChannelSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) { + init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, authorizeNotifications: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeerCategory: @escaping (NotificationsPeerCategory) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateIncludeTag: @escaping (CounterTagSettings, Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, openAppSettings: @escaping () -> Void, updateJoinedNotifications: @escaping (Bool) -> Void, updateNotificationsFromAllAccounts: @escaping (Bool) -> Void) { self.context = context self.presentController = presentController self.pushController = pushController self.soundSelectionDisposable = soundSelectionDisposable self.authorizeNotifications = authorizeNotifications self.suppressWarning = suppressWarning - self.updateMessageAlerts = updateMessageAlerts - self.updateMessagePreviews = updateMessagePreviews - self.updateMessageSound = updateMessageSound - self.updateGroupAlerts = updateGroupAlerts - self.updateGroupPreviews = updateGroupPreviews - self.updateGroupSound = updateGroupSound - self.updateChannelAlerts = updateChannelAlerts - self.updateChannelPreviews = updateChannelPreviews - self.updateChannelSound = updateChannelSound + self.openPeerCategory = openPeerCategory self.updateInAppSounds = updateInAppSounds self.updateInAppVibration = updateInAppVibration self.updateInAppPreviews = updateInAppPreviews @@ -114,7 +94,6 @@ private final class NotificationsAndSoundsArguments { self.updateIncludeTag = updateIncludeTag self.updateTotalUnreadCountCategory = updateTotalUnreadCountCategory self.resetNotifications = resetNotifications - self.updatedExceptionMode = updatedExceptionMode self.openAppSettings = openAppSettings self.updateJoinedNotifications = updateJoinedNotifications self.updateNotificationsFromAllAccounts = updateNotificationsFromAllAccounts @@ -124,9 +103,7 @@ private final class NotificationsAndSoundsArguments { private enum NotificationsAndSoundsSection: Int32 { case accounts case permission - case messages - case groups - case channels + case categories case inApp case displayNamesOnLockscreen case badge @@ -136,12 +113,6 @@ private enum NotificationsAndSoundsSection: Int32 { public enum NotificationsAndSoundsEntryTag: ItemListItemTag { case allAccounts - case messageAlerts - case messagePreviews - case groupAlerts - case groupPreviews - case channelAlerts - case channelPreviews case inAppSounds case inAppVibrate case inAppPreviews @@ -168,27 +139,10 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { case permissionInfo(PresentationTheme, String, String, Bool) case permissionEnable(PresentationTheme, String) - case messageHeader(PresentationTheme, String) - case messageAlerts(PresentationTheme, String, Bool) - case messagePreviews(PresentationTheme, String, Bool) - case messageSound(PresentationTheme, String, String, PeerMessageSound) - case userExceptions(PresentationTheme, PresentationStrings, String, NotificationExceptionMode) - - case messageNotice(PresentationTheme, String) - - case groupHeader(PresentationTheme, String) - case groupAlerts(PresentationTheme, String, Bool) - case groupPreviews(PresentationTheme, String, Bool) - case groupSound(PresentationTheme, String, String, PeerMessageSound) - case groupExceptions(PresentationTheme, PresentationStrings, String, NotificationExceptionMode) - case groupNotice(PresentationTheme, String) - - case channelHeader(PresentationTheme, String) - case channelAlerts(PresentationTheme, String, Bool) - case channelPreviews(PresentationTheme, String, Bool) - case channelSound(PresentationTheme, String, String, PeerMessageSound) - case channelExceptions(PresentationTheme, PresentationStrings, String, NotificationExceptionMode) - case channelNotice(PresentationTheme, String) + case categoriesHeader(PresentationTheme, String) + case privateChats(PresentationTheme, String, String, String) + case groupChats(PresentationTheme, String, String, String) + case channels(PresentationTheme, String, String, String) case inAppHeader(PresentationTheme, String) case inAppSounds(PresentationTheme, String, Bool) @@ -215,12 +169,8 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return NotificationsAndSoundsSection.accounts.rawValue case .permissionInfo, .permissionEnable: return NotificationsAndSoundsSection.permission.rawValue - case .messageHeader, .messageAlerts, .messagePreviews, .messageSound, .messageNotice, .userExceptions: - return NotificationsAndSoundsSection.messages.rawValue - case .groupHeader, .groupAlerts, .groupPreviews, .groupSound, .groupNotice, .groupExceptions: - return NotificationsAndSoundsSection.groups.rawValue - case .channelHeader, .channelAlerts, .channelPreviews, .channelSound, .channelNotice, .channelExceptions: - return NotificationsAndSoundsSection.channels.rawValue + case .categoriesHeader, .privateChats, .groupChats, .channels: + return NotificationsAndSoundsSection.categories.rawValue case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: return NotificationsAndSoundsSection.inApp.rawValue case .displayNamesOnLockscreen, .displayNamesOnLockscreenInfo: @@ -246,70 +196,42 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return 3 case .permissionEnable: return 4 - case .messageHeader: + case .categoriesHeader: return 5 - case .messageAlerts: + case .privateChats: return 6 - case .messagePreviews: + case .groupChats: return 7 - case .messageSound: + case .channels: return 8 - case .userExceptions: - return 9 - case .messageNotice: - return 10 - case .groupHeader: - return 11 - case .groupAlerts: - return 12 - case .groupPreviews: - return 13 - case .groupSound: - return 14 - case .groupExceptions: - return 15 - case .groupNotice: - return 16 - case .channelHeader: - return 17 - case .channelAlerts: - return 18 - case .channelPreviews: - return 19 - case .channelSound: - return 20 - case .channelExceptions: - return 21 - case .channelNotice: - return 22 case .inAppHeader: - return 23 + return 14 case .inAppSounds: - return 24 + return 15 case .inAppVibrate: - return 25 + return 16 case .inAppPreviews: - return 26 + return 17 case .displayNamesOnLockscreen: - return 27 + return 18 case .displayNamesOnLockscreenInfo: - return 28 + return 19 case .badgeHeader: - return 29 + return 20 case .includeChannels: - return 32 + return 21 case .unreadCountCategory: - return 33 + return 22 case .unreadCountCategoryInfo: - return 34 + return 23 case .joinedNotifications: - return 35 + return 24 case .joinedNotificationsInfo: - return 36 + return 25 case .reset: - return 37 + return 26 case .resetNotice: - return 38 + return 27 } } @@ -317,18 +239,6 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { switch self { case .allAccounts: return NotificationsAndSoundsEntryTag.allAccounts - case .messageAlerts: - return NotificationsAndSoundsEntryTag.messageAlerts - case .messagePreviews: - return NotificationsAndSoundsEntryTag.messagePreviews - case .groupAlerts: - return NotificationsAndSoundsEntryTag.groupAlerts - case .groupPreviews: - return NotificationsAndSoundsEntryTag.groupPreviews - case .channelAlerts: - return NotificationsAndSoundsEntryTag.channelAlerts - case .channelPreviews: - return NotificationsAndSoundsEntryTag.channelPreviews case .inAppSounds: return NotificationsAndSoundsEntryTag.inAppSounds case .inAppVibrate: @@ -382,110 +292,26 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } else { return false } - case let .messageHeader(lhsTheme, lhsText): - if case let .messageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .categoriesHeader(lhsTheme, lhsText): + if case let .categoriesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .messageAlerts(lhsTheme, lhsText, lhsValue): - if case let .messageAlerts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .privateChats(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel): + if case let .privateChats(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel { return true } else { return false } - case let .messagePreviews(lhsTheme, lhsText, lhsValue): - if case let .messagePreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .groupChats(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel): + if case let .groupChats(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel { return true } else { return false } - case let .messageSound(lhsTheme, lhsText, lhsValue, lhsSound): - if case let .messageSound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { - return true - } else { - return false - } - case let .userExceptions(lhsTheme, lhsStrings, lhsText, lhsValue): - if case let .userExceptions(rhsTheme, rhsStrings, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .messageNotice(lhsTheme, lhsText): - if case let .messageNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .groupHeader(lhsTheme, lhsText): - if case let .groupHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .groupAlerts(lhsTheme, lhsText, lhsValue): - if case let .groupAlerts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .groupPreviews(lhsTheme, lhsText, lhsValue): - if case let .groupPreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .groupSound(lhsTheme, lhsText, lhsValue, lhsSound): - if case let .groupSound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { - return true - } else { - return false - } - case let .groupExceptions(lhsTheme, lhsStrings, lhsText, lhsValue): - if case let .groupExceptions(rhsTheme, rhsStrings, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .groupNotice(lhsTheme, lhsText): - if case let .groupNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .channelHeader(lhsTheme, lhsText): - if case let .channelHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .channelAlerts(lhsTheme, lhsText, lhsValue): - if case let .channelAlerts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .channelPreviews(lhsTheme, lhsText, lhsValue): - if case let .channelPreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .channelSound(lhsTheme, lhsText, lhsValue, lhsSound): - if case let .channelSound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { - return true - } else { - return false - } - case let .channelExceptions(lhsTheme, lhsStrings, lhsText, lhsValue): - if case let .channelExceptions(rhsTheme, rhsStrings, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .channelNotice(lhsTheme, lhsText): - if case let .channelNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .channels(lhsTheme, lhsTitle, lhsSubtitle, lhsLabel): + if case let .channels(rhsTheme, rhsTitle, rhsSubtitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsLabel == rhsLabel { return true } else { return false @@ -600,81 +426,20 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.authorizeNotifications() }) - case let .messageHeader(_, text): + case let .categoriesHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .messageAlerts(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateMessageAlerts(updatedValue) - }, tag: self.tag) - case let .messagePreviews(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateMessagePreviews(updatedValue) - }, tag: self.tag) - case let .messageSound(_, text, value, sound): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { - let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in - arguments?.updateMessageSound(value) - }) - arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + case let .privateChats(_, title, subtitle, label): + return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/EditProfile"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { + arguments.openPeerCategory(.privateChat) }) - case let .userExceptions(_, strings, text, value): - let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { - let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) - arguments.pushController(controller) + case let .groupChats(_, title, subtitle, label): + return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/GroupChats"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { + arguments.openPeerCategory(.group) }) - case let .messageNotice(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .groupHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .groupAlerts(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateGroupAlerts(updatedValue) - }, tag: self.tag) - case let .groupPreviews(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateGroupPreviews(updatedValue) - }, tag: self.tag) - case let .groupSound(_, text, value, sound): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { - let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in - arguments?.updateGroupSound(value) - }) - arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + case let .channels(_, title, subtitle, label): + return NotificationsCategoryItemListItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels"), title: title, subtitle: subtitle, label: label, sectionId: self.section, style: .blocks, action: { + arguments.openPeerCategory(.channel) }) - case let .groupExceptions(_, strings, text, value): - let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { - let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) - arguments.pushController(controller) - }) - case let .groupNotice(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .channelHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .channelAlerts(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateChannelAlerts(updatedValue) - }, tag: self.tag) - case let .channelPreviews(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in - arguments.updateChannelPreviews(updatedValue) - }, tag: self.tag) - case let .channelSound(_, text, value, sound): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { - let controller = notificationSoundSelectionController(context: arguments.context, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in - arguments?.updateChannelSound(value) - }) - arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - }) - case let .channelExceptions(_, strings, text, value): - let label = value.settings.count > 0 ? strings.Notifications_Exceptions(Int32(value.settings.count)) : strings.Notification_Exceptions_Add - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: label, sectionId: self.section, style: .blocks, action: { - let controller = NotificationExceptionsController(context: arguments.context, mode: value, updatedMode: arguments.updatedExceptionMode) - arguments.pushController(controller) - }) - case let .channelNotice(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .inAppHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .inAppSounds(_, text, value): @@ -768,32 +533,10 @@ private func notificationsAndSoundsEntries(authorizationStatus: AccessType, warn } } - entries.append(.messageHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications.uppercased())) - entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled)) - if globalSettings.privateChats.enabled || !exceptions.users.isEmpty { - entries.append(.messagePreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.privateChats.displayPreviews)) - entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.privateChats.sound)), filteredGlobalSound(globalSettings.privateChats.sound))) - } - entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.users)) - entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptionsHelp)) - - entries.append(.groupHeader(presentationData.theme, presentationData.strings.Notifications_GroupNotifications.uppercased())) - entries.append(.groupAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.groupChats.enabled)) - if globalSettings.groupChats.enabled || !exceptions.groups.isEmpty { - entries.append(.groupPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.groupChats.displayPreviews)) - entries.append(.groupSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.groupChats.sound)), filteredGlobalSound(globalSettings.groupChats.sound))) - } - entries.append(.groupExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.groups)) - entries.append(.groupNotice(presentationData.theme, presentationData.strings.Notifications_GroupNotificationsExceptionsHelp)) - - entries.append(.channelHeader(presentationData.theme, presentationData.strings.Notifications_ChannelNotifications.uppercased())) - entries.append(.channelAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.channels.enabled)) - if globalSettings.channels.enabled || !exceptions.channels.isEmpty { - entries.append(.channelPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.channels.displayPreviews)) - entries.append(.channelSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.channels.sound)), filteredGlobalSound(globalSettings.channels.sound))) - } - entries.append(.channelExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.channels)) - entries.append(.channelNotice(presentationData.theme, presentationData.strings.Notifications_ChannelNotificationsExceptionsHelp)) + entries.append(.categoriesHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications.uppercased())) + entries.append(.privateChats(presentationData.theme, presentationData.strings.Notifications_PrivateChats, !exceptions.users.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.users.peerIds.count)) : "", globalSettings.privateChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) + entries.append(.groupChats(presentationData.theme, presentationData.strings.Notifications_GroupChats, !exceptions.groups.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.groups.peerIds.count)) : "", globalSettings.groupChats.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) + entries.append(.channels(presentationData.theme, presentationData.strings.Notifications_Channels, !exceptions.channels.isEmpty ? presentationData.strings.Notifications_CategoryExceptions(Int32(exceptions.channels.peerIds.count)) : "", globalSettings.channels.enabled ? presentationData.strings.Notifications_On : presentationData.strings.Notifications_Off)) entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications.uppercased())) entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds)) @@ -858,60 +601,30 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Notifications_PermissionsEnable, action: { context.sharedContext.applicationBindings.openSettings() })]), nil) - }, updateMessageAlerts: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.privateChats.enabled = value - return settings - }).start() - }, updateMessagePreviews: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.privateChats.displayPreviews = value - return settings - }).start() - }, updateMessageSound: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.privateChats.sound = value - return settings - }).start() - }, updateGroupAlerts: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.groupChats.enabled = value - return settings - }).start() - }, updateGroupPreviews: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.groupChats.displayPreviews = value - return settings - }).start() - }, updateGroupSound: {value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.groupChats.sound = value - return settings - }).start() - }, updateChannelAlerts: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.channels.enabled = value - return settings - }).start() - }, updateChannelPreviews: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.channels.displayPreviews = value - return settings - }).start() - }, updateChannelSound: {value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.channels.sound = value - return settings - }).start() + }, openPeerCategory: { category in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in + let mode: NotificationExceptionMode + switch category { + case .privateChat: + mode = users + case .group: + mode = groups + case .channel: + mode = channels + } + pushControllerImpl?(notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { mode in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in + switch mode { + case .users: + updateNotificationExceptions((mode, groups, channels)) + case .groups: + updateNotificationExceptions((users, mode, channels)) + case .channels: + updateNotificationExceptions((users, groups, mode)) + } + }) + }, focusOnItemTag: nil)) + }) }, updateInAppSounds: { value in let _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in var settings = settings @@ -977,17 +690,6 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions }) ])]) presentControllerImpl?(actionSheet, nil) - }, updatedExceptionMode: { mode in - _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in - switch mode { - case .users: - updateNotificationExceptions((mode, groups, channels)) - case .groups: - updateNotificationExceptions((users, mode, channels)) - case .channels: - updateNotificationExceptions((users, groups, mode)) - } - }) }, openAppSettings: { context.sharedContext.applicationBindings.openSettings() }, updateJoinedNotifications: { value in diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift new file mode 100644 index 0000000000..011be80fb6 --- /dev/null +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift @@ -0,0 +1,442 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ShimmerEffect +import ItemListUI + +public class NotificationsCategoryItemListItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let icon: UIImage? + let title: String + let subtitle: String + let enabled: Bool + let label: String + public let sectionId: ItemListSectionId + let style: ItemListStyle + let action: (() -> Void)? + public let tag: ItemListItemTag? + public let shimmeringIndex: Int? + + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, subtitle: String, enabled: Bool = true, label: String, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + self.presentationData = presentationData + self.icon = icon + self.title = title + self.subtitle = subtitle + self.enabled = enabled + self.label = label + self.sectionId = sectionId + self.style = style + self.action = action + self.tag = tag + self.shimmeringIndex = shimmeringIndex + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = NotificationsCategoryItemListItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? NotificationsCategoryItemListItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + + if self.enabled { + self.action?() + } + } +} + +public class NotificationsCategoryItemListItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + let iconNode: ASImageNode + let titleNode: TextNode + let subtitleNode: TextNode + let labelNode: TextNode + let arrowNode: ASImageNode + + private let activateArea: AccessibilityAreaNode + + private var item: NotificationsCategoryItemListItem? + + override public var canBeSelected: Bool { + if let item = self.item, let _ = item.action { + return true + } else { + return false + } + } + + public var tag: ItemListItemTag? { + return self.item?.tag + } + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.arrowNode) + + self.addSubnode(self.activateArea) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + + public func asyncLayout() -> (_ item: NotificationsCategoryItemListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + let currentItem = self.item + + return { item, params, neighbors in + let rightInset = 34.0 + params.rightInset + + var updateArrowImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + } + + var updateIcon = false + if currentItem?.icon != item.icon { + updateIcon = true + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + var leftInset = 16.0 + params.leftInset + if let _ = item.icon { + leftInset += 43.0 + } + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let titleColor = item.presentationData.theme.list.itemPrimaryTextColor + let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let detailColor = item.presentationData.theme.list.itemSecondaryTextColor + let labelFont = titleFont + let labelColor = item.presentationData.theme.list.itemSecondaryTextColor + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 60.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let additionalTextRightInset: CGFloat = labelLayout.size.width + + let textConstrain = params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.subtitle, font: detailFont, textColor: detailColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: textConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 1.0 + + let height: CGFloat + if !item.subtitle.isEmpty { + height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + } else { + height = verticalInset * 2.0 + titleLayout.size.height + } + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.label + if item.enabled { + strongSelf.activateArea.accessibilityTraits = [] + } else { + strongSelf.activateArea.accessibilityTraits = .notEnabled + } + + if let icon = item.icon { + if strongSelf.iconNode.supernode == nil { + strongSelf.addSubnode(strongSelf.iconNode) + } + if updateIcon { + strongSelf.iconNode.image = icon + } + let iconY = floorToScreenPixels((layout.contentSize.height - icon.size.height) / 2.0) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: iconY), size: icon.size) + } else if strongSelf.iconNode.supernode != nil { + strongSelf.iconNode.image = nil + strongSelf.iconNode.removeFromSupernode() + } + + if let updateArrowImage = updateArrowImage { + strongSelf.arrowNode.image = updateArrowImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + let _ = subtitleApply() + let _ = labelApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleLayout.size) + strongSelf.subtitleNode.frame = subtitleFrame + + let labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + strongSelf.labelNode.frame = labelFrame + + if let arrowImage = strongSelf.arrowNode.image { + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel)) + + if let shimmeringIndex = item.shimmeringIndex { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + if strongSelf.backgroundNode.supernode != nil { + strongSelf.insertSubnode(shimmerNode, aboveSubnode: strongSelf.backgroundNode) + } else { + strongSelf.addSubnode(shimmerNode) + } + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 + let lineDiameter: CGFloat = 8.0 + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } + } + }) + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted && (self.item?.enabled ?? false) { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift index 971923c44c..ff32d26dab 100644 --- a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift +++ b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift @@ -18,15 +18,27 @@ import TelegramStringFormatting import ItemListPeerItem import ItemListPeerActionItem +private extension PeerMuteState { + var timeInterval: Int32? { + switch self { + case .default: + return nil + case .unmuted: + return 0 + case let .muted(until): + return until + } + } +} + private final class NotificationsPeerCategoryControllerArguments { let context: AccountContext - let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void - let pushController: (ViewController) -> Void let soundSelectionDisposable: MetaDisposable let updateEnabled: (Bool) -> Void let updatePreviews: (Bool) -> Void - let updateSound: (PeerMessageSound) -> Void + + let openSound: (PeerMessageSound) -> Void let addException: () -> Void let openException: (Peer) -> Void @@ -36,15 +48,13 @@ private final class NotificationsPeerCategoryControllerArguments { let updatedExceptionMode: (NotificationExceptionMode) -> Void - init(context: AccountContext, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping (ViewController) -> Void, soundSelectionDisposable: MetaDisposable, updateEnabled: @escaping (Bool) -> Void, updatePreviews: @escaping (Bool) -> Void, updateSound: @escaping (PeerMessageSound) -> Void, addException: @escaping () -> Void, openException: @escaping (Peer) -> Void, removeAllExceptions: @escaping () -> Void, updateRevealedPeerId: @escaping (PeerId?) -> Void, removePeer: @escaping (Peer) -> Void, updatedExceptionMode: @escaping (NotificationExceptionMode) -> Void) { + init(context: AccountContext, soundSelectionDisposable: MetaDisposable, updateEnabled: @escaping (Bool) -> Void, updatePreviews: @escaping (Bool) -> Void, openSound: @escaping (PeerMessageSound) -> Void, addException: @escaping () -> Void, openException: @escaping (Peer) -> Void, removeAllExceptions: @escaping () -> Void, updateRevealedPeerId: @escaping (PeerId?) -> Void, removePeer: @escaping (Peer) -> Void, updatedExceptionMode: @escaping (NotificationExceptionMode) -> Void) { self.context = context - self.presentController = presentController - self.pushController = pushController self.soundSelectionDisposable = soundSelectionDisposable self.updateEnabled = updateEnabled self.updatePreviews = updatePreviews - self.updateSound = updateSound + self.openSound = openSound self.addException = addException self.openException = openException @@ -85,7 +95,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { case exceptionsHeader(PresentationTheme, String) case addException(PresentationTheme, String) - case exception(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, TelegramPeerNotificationSettings, Bool, Bool) + case exception(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, String, TelegramPeerNotificationSettings, Bool, Bool) case removeAllExceptions(PresentationTheme, String) var section: ItemListSectionId { @@ -113,7 +123,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { return 4 case .addException: return 5 - case let .exception(index, _, _, _, _, _, _, _, _): + case let .exception(index, _, _, _, _, _, _, _, _, _): return 6 + index case .removeAllExceptions: return 100000 @@ -171,8 +181,8 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { } else { return false } - case let .exception(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayNameOrder, lhsPeer, lhsSettings, lhsEditing, lhsRevealed): - if case let .exception(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayNameOrder, rhsPeer, rhsSettings, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayNameOrder == rhsDisplayNameOrder, arePeersEqual(lhsPeer, rhsPeer), lhsSettings == rhsSettings, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + case let .exception(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayNameOrder, lhsPeer, lhsDescription, lhsSettings, lhsEditing, lhsRevealed): + if case let .exception(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayNameOrder, rhsPeer, rhsDescription, rhsSettings, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayNameOrder == rhsDisplayNameOrder, arePeersEqual(lhsPeer, rhsPeer), lhsDescription == rhsDescription, lhsSettings == rhsSettings, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false @@ -204,9 +214,9 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updatePreviews(value) }) - case let .sound(_, text, value, _): + case let .sound(_, text, value, sound): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { - + arguments.openSound(sound) }, tag: self.tag) case let .exceptionsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) @@ -214,18 +224,18 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.addException() }) - case let .removeAllExceptions(theme, text): - return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { - arguments.removeAllExceptions() - }) - case let .exception(_, _, _, dateTimeFormat, nameDisplayOrder, peer, _, editing, revealed): - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .text("", .secondary), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + case let .exception(_, _, _, dateTimeFormat, nameDisplayOrder, peer, description, _, editing, revealed): + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .text(description, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { arguments.openException(peer) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in arguments.updateRevealedPeerId(peerId) }, removePeer: { peerId in arguments.removePeer(peer) }, hasTopStripe: false, hasTopGroupInset: false, noInsets: false) + case let .removeAllExceptions(theme, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + arguments.removeAllExceptions() + }) } } } @@ -238,10 +248,11 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound } } -private func notificationsPeerCategoryEntries(category: NotificationsPeerCategory, globalSettings: GlobalNotificationSettingsSet, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode), presentationData: PresentationData) -> [NotificationsPeerCategoryEntry] { +private func notificationsPeerCategoryEntries(category: NotificationsPeerCategory, globalSettings: GlobalNotificationSettingsSet, state: NotificationExceptionState, presentationData: PresentationData) -> [NotificationsPeerCategoryEntry] { var entries: [NotificationsPeerCategoryEntry] = [] let notificationSettings: MessageNotificationSettings + let notificationExceptions = state.mode switch category { case .privateChat: notificationSettings = globalSettings.privateChats @@ -253,14 +264,106 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor entries.append(.enable(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, notificationSettings.enabled)) - if notificationSettings.enabled || !exceptions.users.isEmpty { + if notificationSettings.enabled || !notificationExceptions.isEmpty { entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications.uppercased())) entries.append(.previews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, notificationSettings.displayPreviews)) entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(notificationSettings.sound)), filteredGlobalSound(notificationSettings.sound))) } - + entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications.uppercased())) - + entries.append(.addException(presentationData.theme, presentationData.strings.Notification_Exceptions_AddException)) + + + let sortedExceptions = notificationExceptions.settings.sorted(by: { lhs, rhs in + let lhsName = EnginePeer(lhs.value.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let rhsName = EnginePeer(rhs.value.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + + if let lhsDate = lhs.value.date, let rhsDate = rhs.value.date { + return lhsDate > rhsDate + } else if lhs.value.date != nil && rhs.value.date == nil { + return true + } else if lhs.value.date == nil && rhs.value.date != nil { + return false + } + + if let lhsPeer = lhs.value.peer as? TelegramUser, let rhsPeer = rhs.value.peer as? TelegramUser { + if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil { + return false + } else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil { + return true + } + } + + return lhsName < rhsName + }) + + var existingPeerIds = Set() + var index: Int = 0 + + for (_, value) in sortedExceptions { + if !value.peer.isDeleted { + var title: String + var muted = false + switch value.settings.muteState { + case let .muted(until): + if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + if until < Int32.max - 1 { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: presentationData.strings.baseLanguageCode) + + if Calendar.current.isDateInToday(Date(timeIntervalSince1970: Double(until))) { + formatter.dateFormat = "HH:mm" + } else { + formatter.dateFormat = "E, d MMM HH:mm" + } + + let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(until))) + + title = presentationData.strings.Notification_Exceptions_MutedUntil(dateString).string + } else { + muted = true + title = presentationData.strings.Notification_Exceptions_AlwaysOff + } + } else { + title = presentationData.strings.Notification_Exceptions_AlwaysOn + } + case .unmuted: + title = presentationData.strings.Notification_Exceptions_AlwaysOn + default: + title = "" + } + if !muted { + switch value.settings.messageSound { + case .default: + break + default: + let soundName = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: value.settings.messageSound) + title += (title.isEmpty ? presentationData.strings.Notification_Exceptions_Sound(soundName).string : ", \(presentationData.strings.Notification_Exceptions_Sound(soundName).string)") + } + switch value.settings.displayPreviews { + case .default: + break + default: + if !title.isEmpty { + title += ", " + } + if case .show = value.settings.displayPreviews { + title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn + } else { + title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff + } + } + } + existingPeerIds.insert(value.peer.id) + entries.append(.exception(Int32(index), presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, value.peer, title, value.settings, state.editing, state.revealedPeerId == value.peer.id)) + index += 1 + } + } + + if notificationExceptions.peerIds.count > 0 { + entries.append(.removeAllExceptions(presentationData.theme, presentationData.strings.Notifications_DeleteAllExceptions)) + } + return entries } @@ -270,21 +373,186 @@ public enum NotificationsPeerCategory { case channel } -public func notificationsPeerCategoryController(context: AccountContext, category: NotificationsPeerCategory, exceptionsList: NotificationExceptionsList?, focusOnItemTag: NotificationsPeerCategoryEntryTag? = nil) -> ViewController { - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? +private final class NotificationExceptionState : Equatable { + let mode: NotificationExceptionMode + let isSearchMode: Bool + let revealedPeerId: PeerId? + let editing: Bool + + init(mode: NotificationExceptionMode, isSearchMode: Bool = false, revealedPeerId: PeerId? = nil, editing: Bool = false) { + self.mode = mode + self.isSearchMode = isSearchMode + self.revealedPeerId = revealedPeerId + self.editing = editing + } + + func withUpdatedMode(_ mode: NotificationExceptionMode) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode, isSearchMode: self.isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedSearchMode(_ isSearchMode: Bool) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedEditing(_ editing: Bool) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: self.isSearchMode, revealedPeerId: self.revealedPeerId, editing: editing) + } + + func withUpdatedRevealedPeerId(_ revealedPeerId: PeerId?) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: self.isSearchMode, revealedPeerId: revealedPeerId, editing: self.editing) + } + + func withUpdatedPeerSound(_ peer: Peer, _ sound: PeerMessageSound) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerSound(peer, sound), isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedPeerMuteInterval(_ peer: Peer, _ muteInterval: Int32?) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerMuteInterval(peer, muteInterval), isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedPeerDisplayPreviews(_ peer: Peer, _ displayPreviews: PeerNotificationDisplayPreviews) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerDisplayPreviews(peer, displayPreviews), isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool { + return lhs.mode == rhs.mode && lhs.isSearchMode == rhs.isSearchMode && lhs.revealedPeerId == rhs.revealedPeerId && lhs.editing == rhs.editing + } +} + +public func notificationsPeerCategoryController(context: AccountContext, category: NotificationsPeerCategory, mode: NotificationExceptionMode, updatedMode: @escaping (NotificationExceptionMode) -> Void, focusOnItemTag: NotificationsPeerCategoryEntryTag? = nil) -> ViewController { + var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? + let stateValue = Atomic(value: NotificationExceptionState(mode: mode)) + let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) + + statePromise.set(NotificationExceptionState(mode: mode)) + let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)> = Promise() let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)) -> Void = { value in notificationExceptions.set(.single(value)) } - let arguments = NotificationsPeerCategoryControllerArguments(context: context, presentController: { controller, arguments in - presentControllerImpl?(controller, arguments) - }, pushController: { controller in - pushControllerImpl?(controller) - }, soundSelectionDisposable: MetaDisposable(), updateEnabled: { value in + let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in + let result = stateValue.modify { f($0) } + statePromise.set(result) + updatedMode(result.mode) + } + + let updatePeerSound: (PeerId, PeerMessageSound) -> Signal = { peerId, sound in + return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue + } + + let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal = { peerId, muteInterval in + return context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: muteInterval) |> deliverOnMainQueue + } + + let updatePeerDisplayPreviews:(PeerId, PeerNotificationDisplayPreviews) -> Signal = { + peerId, displayPreviews in + return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, displayPreviews: displayPreviews) |> deliverOnMainQueue + } + + var peerIds: Set = Set(mode.peerIds) + let updateNotificationsDisposable = MetaDisposable() + let updateNotificationsView: (@escaping () -> Void) -> Void = { completion in + updateState { current in + peerIds = peerIds.union(current.mode.peerIds) + let key: PostboxViewKey = .peerNotificationSettings(peerIds: peerIds) + updateNotificationsDisposable.set((context.account.postbox.combinedView(keys: [key]) + |> deliverOnMainQueue).start(next: { view in + if let view = view.views[key] as? PeerNotificationSettingsView { + _ = context.account.postbox.transaction { transaction in + updateState { current in + var current = current + for (key, value) in view.notificationSettings { + if let value = value as? TelegramPeerNotificationSettings { + if let local = current.mode.settings[key] { + if !value.isEqual(to: local.settings), let peer = transaction.getPeer(key), let settings = transaction.getPeerNotificationSettings(key) as? TelegramPeerNotificationSettings, !settings.isEqual(to: local.settings) { + current = current.withUpdatedPeerSound(peer, settings.messageSound).withUpdatedPeerMuteInterval(peer, settings.muteState.timeInterval).withUpdatedPeerDisplayPreviews(peer, settings.displayPreviews) + } + } else if let peer = transaction.getPeer(key) { + if case .default = value.messageSound, case .unmuted = value.muteState, case .default = value.displayPreviews { + } else { + current = current.withUpdatedPeerSound(peer, value.messageSound).withUpdatedPeerMuteInterval(peer, value.muteState.timeInterval).withUpdatedPeerDisplayPreviews(peer, value.displayPreviews) + } + } + } + } + return current + } + }.start(completed: { + completion() + }) + } else { + completion() + } + })) + return current + } + } + + updateNotificationsView({}) + + let presentPeerSettings: (PeerId, @escaping () -> Void) -> Void = { peerId, completion in + let _ = (context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { peer in + completion() + + guard let peer = peer else { + return + } + + let mode = stateValue.with { $0.mode } + pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, mode: mode, updatePeerSound: { peerId, sound in + _ = updatePeerSound(peer.id, sound).start(next: { _ in + updateNotificationsDisposable.set(nil) + _ = combineLatest(updatePeerSound(peer.id, sound), context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { _, peer in + updateState { value in + return value.withUpdatedPeerSound(peer, sound) + } + updateNotificationsView({}) + }) + }) + }, updatePeerNotificationInterval: { peerId, muteInterval in + updateNotificationsDisposable.set(nil) + _ = combineLatest(updatePeerNotificationInterval(peerId, muteInterval), context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { _, peer in + updateState { value in + return value.withUpdatedPeerMuteInterval(peer, muteInterval) + } + updateNotificationsView({}) + }) + }, updatePeerDisplayPreviews: { peerId, displayPreviews in + updateNotificationsDisposable.set(nil) + _ = combineLatest(updatePeerDisplayPreviews(peerId, displayPreviews), context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { _, peer in + updateState { value in + return value.withUpdatedPeerDisplayPreviews(peer, displayPreviews) + } + updateNotificationsView({}) + }) + }, removePeerFromExceptions: { + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peerId]) + |> map { _ -> Peer? in } + |> then(context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + })).start(next: { peer in + guard let peer = peer else { + return + } + updateState { value in + return value.withUpdatedPeerDisplayPreviews(peer, .default).withUpdatedPeerSound(peer, .default).withUpdatedPeerMuteInterval(peer, nil) + } + updateNotificationsView({}) + }) + }, modifiedPeer: { + + })) + }) + } + + let arguments = NotificationsPeerCategoryControllerArguments(context: context, soundSelectionDisposable: MetaDisposable(), updateEnabled: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings switch category { @@ -310,29 +578,104 @@ public func notificationsPeerCategoryController(context: AccountContext, categor } return settings }).start() - }, updateSound: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - switch category { - case .privateChat: - settings.privateChats.sound = value - case .group: - settings.groupChats.sound = value - case .channel: - settings.channels.sound = value - } - return settings - }).start() + }, openSound: { sound in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + switch category { + case .privateChat: + settings.privateChats.sound = value + case .group: + settings.groupChats.sound = value + case .channel: + settings.channels.sound = value + } + return settings + }).start() + }) + pushControllerImpl?(controller) }, addException: { - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] + switch category { + case .privateChat: + filter.insert(.onlyPrivateChats) + filter.insert(.excludeSavedMessages) + filter.insert(.excludeSecretChats) + case .group: + filter.insert(.onlyGroups) + case .channel: + filter.insert(.onlyChannels) + } + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle)) + controller.peerSelected = { [weak controller] peer in + let peerId = peer.id + + presentPeerSettings(peerId, { + controller?.dismiss() + }) + } + pushControllerImpl?(controller) }, openException: { peer in - + presentPeerSettings(peer.id, {}) }, removeAllExceptions: { - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.Notification_Exceptions_DeleteAllConfirmation), + ActionSheetButtonItem(title: presentationData.strings.Notification_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let values = stateValue.with { $0.mode.settings.values } + let _ = (context.account.postbox.transaction { transaction -> Void in + for value in values { + if transaction.getPeer(value.peer.id) == nil { + updatePeers(transaction: transaction, peers: [value.peer], update: { _, updated in + updated + }) + } + } + } + |> deliverOnMainQueue).start(completed: { + updateNotificationsDisposable.set(nil) + updateState { state in + var state = state + for value in values { + state = state.withUpdatedPeerMuteInterval(value.peer, nil).withUpdatedPeerSound(value.peer, .default).withUpdatedPeerDisplayPreviews(value.peer, .default) + } + return state + } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: values.map(\.peer.id)) + |> deliverOnMainQueue).start(completed: { + updateNotificationsView({}) + }) + }) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) }, updateRevealedPeerId: { peerId in - + updateState { current in + return current.withUpdatedRevealedPeerId(peerId) + } }, removePeer: { peer in - + _ = (context.account.postbox.transaction { transaction in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { _, updated in return updated}) + } + } |> deliverOnMainQueue).start(completed: { + updateNotificationsDisposable.set(nil) + updateState { value in + return value.withUpdatedPeerMuteInterval(peer, nil).withUpdatedPeerSound(peer, .default).withUpdatedPeerDisplayPreviews(peer, .default) + } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peer.id]) + |> deliverOnMainQueue).start(completed: { + updateNotificationsView({}) + }) + }) }, updatedExceptionMode: { mode in _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in switch mode { @@ -349,77 +692,32 @@ public func notificationsPeerCategoryController(context: AccountContext, categor let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) - let exceptionsSignal = Signal.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init)) - - notificationExceptions.set(exceptionsSignal |> map { list -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in - var users:[PeerId : NotificationExceptionWrapper] = [:] - var groups: [PeerId : NotificationExceptionWrapper] = [:] - var channels:[PeerId : NotificationExceptionWrapper] = [:] - if let list = list { - for (key, value) in list.settings { - if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { - switch value.muteState { - case .default: - switch value.messageSound { - case .default: - break - default: - switch key.namespace { - case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: peer) - default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - channels[key] = NotificationExceptionWrapper(settings: value, peer: peer) - } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) - } - } - } - default: - switch key.namespace { - case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: peer) - default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - channels[key] = NotificationExceptionWrapper(settings: value, peer: peer) - } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) - } - } - } + let signal = combineLatest(context.sharedContext.presentationData, sharedData, preferences, statePromise.get()) + |> map { presentationData, sharedData, view, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let viewSettings: GlobalNotificationSettingsSet + if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { + viewSettings = settings.effective + } else { + viewSettings = GlobalNotificationSettingsSet.defaultSettings + } + + let entries = notificationsPeerCategoryEntries(category: category, globalSettings: viewSettings, state: state, presentationData: presentationData) + + var index = 0 + var scrollToItem: ListViewScrollToItem? + if let focusOnItemTag = focusOnItemTag { + for entry in entries { + if entry.tag?.isEqual(to: focusOnItemTag) ?? false { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) } + index += 1 } } - return (.users(users), .groups(groups), .channels(channels)) - }) - - let signal = combineLatest(context.sharedContext.presentationData, sharedData, preferences, notificationExceptions.get()) - |> map { presentationData, sharedData, view, exceptions -> (ItemListControllerState, (ItemListNodeState, Any)) in - let viewSettings: GlobalNotificationSettingsSet - if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { - viewSettings = settings.effective - } else { - viewSettings = GlobalNotificationSettingsSet.defaultSettings - } - - let entries = notificationsPeerCategoryEntries(category: category, globalSettings: viewSettings, exceptions: exceptions, presentationData: presentationData) - - var index = 0 - var scrollToItem: ListViewScrollToItem? - if let focusOnItemTag = focusOnItemTag { - for entry in entries { - if entry.tag?.isEqual(to: focusOnItemTag) ?? false { - scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) - } - index += 1 - } - } - - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem) - - return (controllerState, (listState, arguments)) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToItem) + + return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 2f66459456..a8c4dbe046 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -331,12 +331,12 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob SettingsSearchableItem(id: .notifications(0), title: strings.Settings_NotificationsAndSounds, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_Title), icon: icon, breadcrumbs: [], present: { context, _, present in presentNotificationSettings(context, present, nil) }), - SettingsSearchableItem(id: .notifications(1), title: strings.Notifications_MessageNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .messageAlerts) - }), - SettingsSearchableItem(id: .notifications(2), title: strings.Notifications_MessageNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .messagePreviews) - }), +// SettingsSearchableItem(id: .notifications(1), title: strings.Notifications_MessageNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .messageAlerts) +// }), +// SettingsSearchableItem(id: .notifications(2), title: strings.Notifications_MessageNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .messagePreviews) +// }), SettingsSearchableItem(id: .notifications(3), title: strings.Notifications_MessageNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.privateChats.sound), defaultSound: nil, completion: { value in @@ -351,12 +351,12 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob SettingsSearchableItem(id: .notifications(4), title: strings.Notifications_MessageNotificationsExceptions, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in present(.push, NotificationExceptionsController(context: context, mode: exceptions().0, updatedMode: { _ in})) }), - SettingsSearchableItem(id: .notifications(5), title: strings.Notifications_GroupNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .groupAlerts) - }), - SettingsSearchableItem(id: .notifications(6), title: strings.Notifications_GroupNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .groupPreviews) - }), +// SettingsSearchableItem(id: .notifications(5), title: strings.Notifications_GroupNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .groupAlerts) +// }), +// SettingsSearchableItem(id: .notifications(6), title: strings.Notifications_GroupNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .groupPreviews) +// }), SettingsSearchableItem(id: .notifications(7), title: strings.Notifications_GroupNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.groupChats.sound), defaultSound: nil, completion: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in @@ -370,12 +370,12 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob SettingsSearchableItem(id: .notifications(8), title: strings.Notifications_GroupNotificationsExceptions, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in present(.push, NotificationExceptionsController(context: context, mode: exceptions().1, updatedMode: { _ in})) }), - SettingsSearchableItem(id: .notifications(9), title: strings.Notifications_ChannelNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .channelAlerts) - }), - SettingsSearchableItem(id: .notifications(10), title: strings.Notifications_ChannelNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .channelPreviews) - }), +// SettingsSearchableItem(id: .notifications(9), title: strings.Notifications_ChannelNotificationsAlert, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsAlert), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .channelAlerts) +// }), +// SettingsSearchableItem(id: .notifications(10), title: strings.Notifications_ChannelNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in +// presentNotificationSettings(context, present, .channelPreviews) +// }), SettingsSearchableItem(id: .notifications(11), title: strings.Notifications_ChannelNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.channels.sound), defaultSound: nil, completion: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Contents.json new file mode 100644 index 0000000000..ac0340ba52 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon-35.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Icon-35.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Icon-35.pdf new file mode 100644 index 0000000000..1829a07d65 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Channels.imageset/Icon-35.pdf @@ -0,0 +1,126 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.584314 0.000000 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.000000 5.493408 cm +1.000000 1.000000 1.000000 scn +4.000000 6.005815 m +6.634244 6.005815 l +6.966668 6.005815 7.132880 6.005815 7.294138 5.988326 c +7.738360 5.940148 8.166211 5.793349 8.546436 5.558653 c +8.684463 5.473454 8.815662 5.371410 9.078063 5.167321 c +11.835391 3.022733 l +11.835403 3.022722 l +13.492669 1.733738 14.321304 1.089245 15.017017 1.095165 c +15.622235 1.100315 16.192566 1.379257 16.568199 1.853825 c +17.000000 2.399353 17.000000 3.449122 17.000000 5.548661 c +17.000000 14.462967 l +17.000000 16.562506 17.000000 17.612276 16.568199 18.157804 c +16.192566 18.632374 15.622235 18.911314 15.017017 18.916464 c +14.321305 18.922384 13.492673 18.277893 11.835412 16.988913 c +11.835398 16.988903 l +11.835389 16.988895 l +9.078062 14.844307 l +8.815662 14.640219 8.684463 14.538175 8.546436 14.452976 c +8.166211 14.218279 7.738360 14.071481 7.294138 14.023304 c +7.132880 14.005815 6.966668 14.005815 6.634244 14.005815 c +4.000000 14.005815 l +1.790861 14.005815 0.000000 12.214953 0.000000 10.005815 c +0.000000 7.796676 1.790861 6.005815 4.000000 6.005815 c +h +4.094136 4.100051 m +4.000000 3.898727 4.000000 3.639016 4.000000 3.119593 c +4.000000 2.005812 l +4.000000 0.901243 4.895431 0.005812 6.000000 0.005812 c +7.104569 0.005812 8.000000 0.901243 8.000000 2.005812 c +8.000000 3.119593 l +8.000000 3.639016 8.000000 3.898727 7.905864 4.100051 c +7.806588 4.312369 7.635887 4.483070 7.423568 4.582348 c +7.222244 4.676483 6.962533 4.676483 6.443110 4.676483 c +5.556890 4.676483 l +5.037467 4.676483 4.777756 4.676483 4.576432 4.582348 c +4.364113 4.483070 4.193412 4.312369 4.094136 4.100051 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2587 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002677 00000 n +0000002700 00000 n +0000002873 00000 n +0000002947 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3006 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Contents.json new file mode 100644 index 0000000000..5545732fb9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon-34.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Icon-34.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Icon-34.pdf new file mode 100644 index 0000000000..3b6bc51c2f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/GroupChats.imageset/Icon-34.pdf @@ -0,0 +1,136 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.203922 0.780392 0.349020 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.652100 8.000000 cm +1.000000 1.000000 1.000000 scn +15.347914 11.500000 m +15.347914 9.843145 14.004768 8.500000 12.347914 8.500000 c +10.691060 8.500000 9.347914 9.843145 9.347914 11.500000 c +9.347914 13.156855 10.691060 14.500000 12.347914 14.500000 c +14.004768 14.500000 15.347914 13.156855 15.347914 11.500000 c +h +16.206648 5.096860 m +17.131365 4.487454 17.732334 3.680024 18.122902 2.887923 c +18.464052 2.196046 18.353537 1.522406 17.970762 1.000000 c +17.531406 0.400373 16.733355 -0.000002 15.847914 -0.000002 c +8.847914 -0.000002 l +7.962472 -0.000002 7.164421 0.400373 6.725066 1.000000 c +6.701973 1.031517 6.679871 1.063584 6.658800 1.096172 c +6.330600 1.603753 6.252357 2.237787 6.572926 2.887924 c +6.987937 3.729597 7.640509 4.588578 8.666627 5.208897 c +9.571874 5.756145 10.767847 6.117645 12.347914 6.117645 c +13.927979 6.117645 15.123951 5.756145 16.029198 5.208899 c +16.089634 5.172363 16.148775 5.134999 16.206648 5.096860 c +h +5.347910 6.000000 m +6.038868 6.000000 6.650654 5.923974 7.192287 5.790671 c +6.346507 5.094717 5.770174 4.267296 5.380054 3.476105 c +4.957832 2.619810 4.950275 1.751801 5.228493 1.000000 c +2.054037 1.000000 l +0.595407 1.000000 -0.530585 2.298071 0.261390 3.522970 c +1.064612 4.765267 2.563349 6.000000 5.347910 6.000000 c +h +19.315775 3.476104 m +19.737995 2.619809 19.745552 1.751801 19.467335 1.000000 c +22.641785 1.000000 l +24.100414 1.000000 25.226404 2.298071 24.434431 3.522971 c +23.631208 4.765267 22.132471 6.000000 19.347910 6.000000 c +18.656954 6.000000 18.045172 5.923975 17.503540 5.790672 c +18.349321 5.094717 18.925655 4.267296 19.315775 3.476104 c +h +7.847914 10.500000 m +7.847914 9.119288 6.728626 8.000000 5.347914 8.000000 c +3.967202 8.000000 2.847914 9.119288 2.847914 10.500000 c +2.847914 11.880713 3.967202 13.000000 5.347914 13.000000 c +6.728626 13.000000 7.847914 11.880713 7.847914 10.500000 c +h +21.847914 10.500000 m +21.847914 9.119288 20.728626 8.000000 19.347914 8.000000 c +17.967201 8.000000 16.847914 9.119288 16.847914 10.500000 c +16.847914 11.880713 17.967201 13.000000 19.347914 13.000000 c +20.728626 13.000000 21.847914 11.880713 21.847914 10.500000 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3113 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003203 00000 n +0000003226 00000 n +0000003399 00000 n +0000003473 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3532 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index cee8ba624d..885b84e0ba 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -6356,7 +6356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, callMessages: self.callMessages) contentHeight += headerHeight - contentHeight += sectionSpacing + if !(self.isSettings && self.state.isEditing) { + contentHeight += sectionSpacing + } for (sectionId, sectionItems) in items { validRegularSections.append(sectionId)