diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a960050c2a..9bdeb2b342 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11896,6 +11896,7 @@ Sorry for the inconvenience."; "BusinessLink.ErrorExpired" = "Link Expired"; "WebApp.AlertBiometryAccessText" = "Do you want to allow %@ to use Face ID?"; +"WebApp.AlertBiometryAccessTouchIDText" = "Do you want to allow %@ to use Touch ID?"; "StoryList.SubtitleArchived_1" = "1 archived post"; "StoryList.SubtitleArchived_any" = "%d archived posts"; @@ -12089,3 +12090,6 @@ Sorry for the inconvenience."; "Map.SharingLocation" = "Sharing Location..."; "Channel.AdminLog.Settings" = "Settings"; + +"ChannelProfile.AboutActionCopy" = "Copy"; +"ChannelProfile.ToastAboutCopied" = "Copied to clipboard."; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h index a5eff4d4f4..469cc068ff 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h @@ -5,5 +5,6 @@ - (void)setPositiveContentColor:(UIColor *)color; - (void)setNegativeContentColor:(UIColor *)color; +- (void)updateIsLocked:(bool)isLocked; @end diff --git a/submodules/LegacyComponents/Sources/TGIconSwitchView.m b/submodules/LegacyComponents/Sources/TGIconSwitchView.m index a96b6c5787..73a23c727e 100644 --- a/submodules/LegacyComponents/Sources/TGIconSwitchView.m +++ b/submodules/LegacyComponents/Sources/TGIconSwitchView.m @@ -30,8 +30,10 @@ static const void *positionChangedKey = &positionChangedKey; @interface TGIconSwitchView () { UIImageView *_offIconView; UIImageView *_onIconView; + UIColor *_negativeContentColor; bool _stateIsOn; + bool _isLocked; } @end @@ -60,13 +62,8 @@ static const void *positionChangedKey = &positionChangedKey; object_setClass(handleView.layer, subclass); }); - CGPoint offset = CGPointZero; - if (iosMajorVersion() >= 12) { - offset = CGPointMake(-7.0, -3.0); - } + [self updateIconFrame]; - _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y); - _onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y); [handleView addSubview:_onIconView]; [handleView addSubview:_offIconView]; @@ -92,6 +89,21 @@ static const void *positionChangedKey = &positionChangedKey; [self updateState:on animated:animated force:true]; } +- (void)updateIconFrame { + CGPoint offset = CGPointZero; + if (iosMajorVersion() >= 12) { + offset = CGPointMake(-7.0, -3.0); + } + + if (_isLocked) { + _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(12.5f) + offset.y); + } else { + _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y); + } + + _onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y); +} + - (void)updateState:(bool)on animated:(bool)animated force:(bool)force { if (_stateIsOn != on || force) { _stateIsOn = on; @@ -125,7 +137,24 @@ static const void *positionChangedKey = &positionChangedKey; } - (void)setNegativeContentColor:(UIColor *)color { - _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color); + _negativeContentColor = color; + if (_isLocked) { + _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"Item List/SwitchLockIcon"), color); + _offIconView.frame = CGRectMake(_offIconView.frame.origin.x, _offIconView.frame.origin.y, _offIconView.image.size.width, _offIconView.image.size.height); + } else { + _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color); + } +} + +- (void)updateIsLocked:(bool)isLocked { + if (_isLocked != isLocked) { + _isLocked = isLocked; + + if (_negativeContentColor) { + [self setNegativeContentColor:_negativeContentColor]; + } + [self updateIconFrame]; + } } - (void)currentValueChanged { diff --git a/submodules/SwitchNode/Sources/IconSwitchNode.swift b/submodules/SwitchNode/Sources/IconSwitchNode.swift index bdf0fe9db9..83b3b46d61 100644 --- a/submodules/SwitchNode/Sources/IconSwitchNode.swift +++ b/submodules/SwitchNode/Sources/IconSwitchNode.swift @@ -69,6 +69,8 @@ open class IconSwitchNode: ASDisplayNode { } } + private var _isLocked: Bool = false + override public init() { super.init() @@ -82,8 +84,8 @@ open class IconSwitchNode: ASDisplayNode { (self.view as! UISwitch).backgroundColor = self.backgroundColor (self.view as! UISwitch).tintColor = self.frameColor - //(self.view as! UISwitch).thumbTintColor = self.handleColor (self.view as! UISwitch).onTintColor = self.contentColor + (self.view as? TGIconSwitchView)?.updateIsLocked(self._isLocked) (self.view as! IconSwitchNodeView).setNegativeContentColor(self.negativeContentColor) (self.view as! IconSwitchNodeView).setPositiveContentColor(self.positiveContentColor) @@ -99,6 +101,16 @@ open class IconSwitchNode: ASDisplayNode { } } + public func updateIsLocked(_ value: Bool) { + if self._isLocked == value { + return + } + self._isLocked = value + if self.isNodeLoaded { + (self.view as? TGIconSwitchView)?.updateIsLocked(value) + } + } + override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: 51.0, height: 31.0) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 710d3568a3..60e2e1dc3c 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -317,6 +317,8 @@ public enum PresentationResourceKey: Int32 { case hideIconImage case peerStatusLockedImage + case expandDownArrowImage + case expandSmallDownArrowImage } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 91f5f1c975..81e5b19064 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -414,4 +414,16 @@ public struct PresentationResourcesItemList { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.list.itemSecondaryTextColor) }) } + + public static func expandDownArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.expandDownArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Item List/ExpandingItemVerticalRegularArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } + + public static func expandSmallDownArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.expandSmallDownArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Item List/ExpandingItemVerticalSmallArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index bb37bc8c81..a1ad4fef95 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -20,16 +20,171 @@ import ListSectionComponent import ListActionItemComponent import PlainButtonComponent -private let banSendMediaFlags: TelegramChatBannedRightsFlags = [ - .banSendPhotos, - .banSendVideos, - .banSendGifs, - .banSendMusic, - .banSendFiles, - .banSendVoice, - .banSendInstantVideos, - .banEmbedLinks, - .banSendPolls +struct MediaRight: OptionSet, Hashable { + var rawValue: Int + + static let photos = MediaRight(rawValue: 1 << 0) + static let videos = MediaRight(rawValue: 1 << 1) + static let stickersAndGifs = MediaRight(rawValue: 1 << 2) + static let music = MediaRight(rawValue: 1 << 3) + static let files = MediaRight(rawValue: 1 << 4) + static let voiceMessages = MediaRight(rawValue: 1 << 5) + static let videoMessages = MediaRight(rawValue: 1 << 6) + static let links = MediaRight(rawValue: 1 << 7) + static let polls = MediaRight(rawValue: 1 << 8) +} + +extension MediaRight { + var count: Int { + var result = 0 + var index = 0 + while index < 31 { + let currentValue = self.rawValue >> UInt32(index) + index += 1 + if currentValue == 0 { + break + } + + if (currentValue & 1) != 0 { + result += 1 + } + } + return result + } +} + +private struct ParticipantRight: OptionSet { + var rawValue: Int + + static let sendMessages = ParticipantRight(rawValue: 1 << 0) + static let addMembers = ParticipantRight(rawValue: 1 << 2) + static let pinMessages = ParticipantRight(rawValue: 1 << 3) + static let changeInfo = ParticipantRight(rawValue: 1 << 4) +} + +private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) -> (participantRights: ParticipantRight, mediaRights: MediaRight) { + var participantResult: ParticipantRight = [ + .sendMessages, + .addMembers, + .pinMessages, + .changeInfo + ] + var mediaResult: MediaRight = [ + .photos, + .videos, + .stickersAndGifs, + .music, + .files, + .voiceMessages, + .videoMessages, + .links, + .polls + ] + + if rights.contains(.banSendText) { + participantResult.remove(.sendMessages) + } + if rights.contains(.banAddMembers) { + participantResult.remove(.addMembers) + } + if rights.contains(.banPinMessages) { + participantResult.remove(.pinMessages) + } + if rights.contains(.banChangeInfo) { + participantResult.remove(.changeInfo) + } + + if rights.contains(.banSendPhotos) { + mediaResult.remove(.photos) + } + if rights.contains(.banSendVideos) { + mediaResult.remove(.videos) + } + if rights.contains(.banSendStickers) || rights.contains(.banSendGifs) || rights.contains(.banSendGames) || rights.contains(.banSendInline) { + mediaResult.remove(.stickersAndGifs) + } + if rights.contains(.banSendMusic) { + mediaResult.remove(.music) + } + if rights.contains(.banSendFiles) { + mediaResult.remove(.files) + } + if rights.contains(.banSendVoice) { + mediaResult.remove(.voiceMessages) + } + if rights.contains(.banSendInstantVideos) { + mediaResult.remove(.videoMessages) + } + if rights.contains(.banEmbedLinks) { + mediaResult.remove(.links) + } + if rights.contains(.banSendPolls) { + mediaResult.remove(.polls) + } + + return (participantResult, mediaResult) +} + +private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRights: MediaRight) -> TelegramChatBannedRightsFlags { + var result: TelegramChatBannedRightsFlags = [] + + if !participantRights.contains(.sendMessages) { + result.insert(.banSendText) + } + if !participantRights.contains(.addMembers) { + result.insert(.banAddMembers) + } + if !participantRights.contains(.pinMessages) { + result.insert(.banPinMessages) + } + if !participantRights.contains(.changeInfo) { + result.insert(.banChangeInfo) + } + + if !mediaRights.contains(.photos) { + result.insert(.banSendPhotos) + } + if !mediaRights.contains(.videos) { + result.insert(.banSendVideos) + } + if !mediaRights.contains(.stickersAndGifs) { + result.insert(.banSendStickers) + result.insert(.banSendGifs) + result.insert(.banSendGames) + result.insert(.banSendInline) + } + if !mediaRights.contains(.music) { + result.insert(.banSendMusic) + } + if !mediaRights.contains(.files) { + result.insert(.banSendFiles) + } + if !mediaRights.contains(.voiceMessages) { + result.insert(.banSendVoice) + } + if !mediaRights.contains(.videoMessages) { + result.insert(.banSendInstantVideos) + } + if !mediaRights.contains(.links) { + result.insert(.banEmbedLinks) + } + if !mediaRights.contains(.polls) { + result.insert(.banSendPolls) + } + + return result +} + +private let allMediaRightItems: [MediaRight] = [ + .photos, + .videos, + .stickersAndGifs, + .music, + .files, + .voiceMessages, + .videoMessages, + .links, + .polls ] private final class AdminUserActionsSheetComponent: Component { @@ -37,14 +192,14 @@ private final class AdminUserActionsSheetComponent: Component { let context: AccountContext let chatPeer: EnginePeer - let peers: [EnginePeer] + let peers: [RenderedChannelParticipant] let messageCount: Int let completion: (AdminUserActionsSheet.Result) -> Void init( context: AccountContext, chatPeer: EnginePeer, - peers: [EnginePeer], + peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (AdminUserActionsSheet.Result) -> Void ) { @@ -131,11 +286,12 @@ private final class AdminUserActionsSheetComponent: Component { private var optionBanSelectedPeers = Set() private var isConfigurationExpanded: Bool = false - private var configSendMessages: Bool = false - private var configSendMedia: Bool = false - private var configAddUsers: Bool = false - private var configPinMessages: Bool = false - private var configChangeInfo: Bool = false + private var isMediaSectionExpanded: Bool = false + + private var allowedParticipantRights: ParticipantRight = [] + private var allowedMediaRights: MediaRight = [] + private var participantRights: ParticipantRight = [] + private var mediaRights: MediaRight = [] private var previousWasConfigurationExpanded: Bool = false @@ -263,22 +419,7 @@ private final class AdminUserActionsSheetComponent: Component { } } else { var banFlags: TelegramChatBannedRightsFlags = [] - - if !self.configSendMessages { - banFlags.insert(.banSendText) - } - if !self.configSendMedia { - banFlags.formUnion(banSendMediaFlags) - } - if !self.configAddUsers { - banFlags.insert(.banAddMembers) - } - if !self.configPinMessages { - banFlags.insert(.banPinMessages) - } - if !self.configChangeInfo { - banFlags.insert(.banChangeInfo) - } + banFlags = rightFlagsFromRights(participantRights: self.participantRights, mediaRights: self.mediaRights) let bannedRights = TelegramChatBannedRights(flags: banFlags, untilDate: Int32.max) for id in self.optionBanSelectedPeers.sorted() { @@ -370,6 +511,44 @@ private final class AdminUserActionsSheetComponent: Component { let sideInset: CGFloat = 16.0 if self.component == nil { + var (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([]) + if case let .channel(channel) = component.chatPeer { + (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights(channel.defaultBannedRights?.flags ?? []) + } + + var (commonParticipantRights, commonMediaRights) = rightsFromBannedRights([]) + + loop: for peer in component.peers { + var (peerParticipantRights, peerMediaRights) = rightsFromBannedRights([]) + switch peer.participant { + case .creator: + allowedParticipantRights = [] + allowedMediaRights = [] + break loop + case let .member(_, _, adminInfo, banInfo, _): + if adminInfo != nil { + allowedParticipantRights = [] + allowedMediaRights = [] + break loop + } else if let banInfo { + (peerParticipantRights, peerMediaRights) = rightsFromBannedRights(banInfo.rights.flags) + } + } + peerParticipantRights = peerParticipantRights.intersection(allowedParticipantRights) + peerMediaRights = peerMediaRights.intersection(allowedMediaRights) + + commonParticipantRights = commonParticipantRights.intersection(peerParticipantRights) + commonMediaRights = commonMediaRights.intersection(peerMediaRights) + } + + commonParticipantRights = commonParticipantRights.intersection(allowedParticipantRights) + commonMediaRights = commonMediaRights.intersection(allowedMediaRights) + + self.allowedParticipantRights = allowedParticipantRights + self.participantRights = commonParticipantRights + + self.allowedMediaRights = allowedMediaRights + self.mediaRights = commonMediaRights } self.component = component @@ -423,6 +602,43 @@ private final class AdminUserActionsSheetComponent: Component { case ban } + var availableOptions: [OptionsSection] = [] + availableOptions.append(.report) + + if case let .channel(channel) = component.chatPeer { + if channel.hasPermission(.deleteAllMessages) { + availableOptions.append(.deleteAll) + + if channel.hasPermission(.banMembers) { + var canBanEveryone = true + for peer in component.peers { + if peer.peer.id == component.context.account.peerId { + canBanEveryone = false + continue + } + + switch peer.participant { + case .creator: + canBanEveryone = false + case let .member(_, _, adminInfo, banInfo, _): + let _ = banInfo + if let adminInfo { + if channel.flags.contains(.isCreator) { + } else if adminInfo.promotedBy == component.context.account.peerId { + } else { + canBanEveryone = false + } + } + } + } + + if canBanEveryone { + availableOptions.append(.ban) + } + } + } + } + let optionsItem: (OptionsSection) -> AnyComponentWithIdentity = { section in let sectionId: AnyHashable let selectedPeers: Set @@ -442,7 +658,7 @@ private final class AdminUserActionsSheetComponent: Component { isExpanded = self.isOptionDeleteAllExpanded if component.peers.count == 1 { - title = "Delete All from \(component.peers[0].compactDisplayTitle)" + title = "Delete All from \(EnginePeer(component.peers[0].peer).compactDisplayTitle)" } else { title = "Delete All from Users" } @@ -454,8 +670,8 @@ private final class AdminUserActionsSheetComponent: Component { let banTitle: String let restrictTitle: String if component.peers.count == 1 { - banTitle = "Ban \(component.peers[0].compactDisplayTitle)" - restrictTitle = "Restrict \(component.peers[0].compactDisplayTitle)" + banTitle = "Ban \(EnginePeer(component.peers[0].peer).compactDisplayTitle)" + restrictTitle = "Restrict \(EnginePeer(component.peers[0].peer).compactDisplayTitle)" } else { banTitle = "Ban Users" restrictTitle = "Restrict Users" @@ -527,7 +743,7 @@ private final class AdminUserActionsSheetComponent: Component { if selectedPeers.isEmpty { for peer in component.peers { - selectedPeers.insert(peer.id) + selectedPeers.insert(peer.peer.id) } } else { selectedPeers.removeAll() @@ -567,7 +783,7 @@ private final class AdminUserActionsSheetComponent: Component { if selectedPeers.isEmpty { for peer in component.peers { - selectedPeers.insert(peer.id) + selectedPeers.insert(peer.peer.id) } } else { selectedPeers.removeAll() @@ -605,14 +821,14 @@ private final class AdminUserActionsSheetComponent: Component { var peerItems: [AnyComponentWithIdentity] = [] for peer in component.peers { - peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent( + peerItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(AdminUserActionsPeerComponent( context: component.context, theme: environment.theme, strings: environment.strings, sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - selectionState: .editing(isSelected: selectedPeers.contains(peer.id)), + title: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: EnginePeer(peer.peer), + selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)), action: { [weak self] peer in guard let self else { return @@ -653,7 +869,7 @@ private final class AdminUserActionsSheetComponent: Component { items: peerItems ))) } - + //TODO:localize let titleString: String if component.messageCount == 1 { @@ -684,19 +900,21 @@ private final class AdminUserActionsSheetComponent: Component { var optionsSectionItems: [AnyComponentWithIdentity] = [] - optionsSectionItems.append(optionsItem(.report)) - if self.isOptionReportExpanded { - optionsSectionItems.append(expandedPeersItem(.report)) - } - - optionsSectionItems.append(optionsItem(.deleteAll)) - if self.isOptionDeleteAllExpanded { - optionsSectionItems.append(expandedPeersItem(.deleteAll)) - } - - optionsSectionItems.append(optionsItem(.ban)) - if self.isOptionBanExpanded { - optionsSectionItems.append(expandedPeersItem(.ban)) + for option in availableOptions { + let isOptionExpanded: Bool + switch option { + case .report: + isOptionExpanded = self.isOptionReportExpanded + case .deleteAll: + isOptionExpanded = self.isOptionDeleteAllExpanded + case .ban: + isOptionExpanded = self.isOptionBanExpanded + } + + optionsSectionItems.append(optionsItem(option)) + if isOptionExpanded { + optionsSectionItems.append(expandedPeersItem(option)) + } } var optionsSectionTransition = transition @@ -748,7 +966,7 @@ private final class AdminUserActionsSheetComponent: Component { component: AnyComponent(PlainButtonComponent( content: AnyComponent(OptionsSectionFooterComponent( theme: environment.theme, - text: self.isConfigurationExpanded ? fullyBanTitle : partiallyRestrictTitle, + text: self.isConfigurationExpanded ? partiallyRestrictTitle : fullyBanTitle, fontSize: presentationData.listsFontSize.itemListBaseHeaderFontSize, isExpanded: self.isConfigurationExpanded )), @@ -761,7 +979,7 @@ private final class AdminUserActionsSheetComponent: Component { self.isConfigurationExpanded = !self.isConfigurationExpanded if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty { for peer in component.peers { - self.optionBanSelectedPeers.insert(peer.id) + self.optionBanSelectedPeers.insert(peer.peer.id) } } self.state?.updated(transition: .spring(duration: 0.35)) @@ -776,7 +994,7 @@ private final class AdminUserActionsSheetComponent: Component { var configSectionItems: [AnyComponentWithIdentity] = [] - enum ConfigItem: Hashable { + enum ConfigItem: Hashable, CaseIterable { case sendMessages case sendMedia case addUsers @@ -784,93 +1002,238 @@ private final class AdminUserActionsSheetComponent: Component { case changeInfo } - let allConfigItems: [ConfigItem] = [ - .sendMessages, - .sendMedia, - .addUsers, - .pinMessages, - .changeInfo - ] - if case let .channel(channel) = component.chatPeer { - let defaultBannedFlags = channel.defaultBannedRights?.flags ?? [] - - loop: for configItem in allConfigItems { - let itemTitle: String - let itemValue: Bool + var allConfigItems: [(ConfigItem, Bool)] = [] + if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { + for configItem in ConfigItem.allCases { + let isEnabled: Bool switch configItem { case .sendMessages: - if defaultBannedFlags.contains(.banSendText) { - continue loop - } - - itemTitle = "Send Text Messages" - itemValue = self.configSendMessages + isEnabled = self.allowedParticipantRights.contains(.sendMessages) case .sendMedia: - if !defaultBannedFlags.intersection(banSendMediaFlags).isEmpty { - continue loop - } - - itemTitle = "Send Media" - itemValue = self.configSendMedia + isEnabled = !self.allowedMediaRights.isEmpty case .addUsers: - if defaultBannedFlags.contains(.banAddMembers) { - continue loop - } - - itemTitle = "Add Users" - itemValue = self.configAddUsers + isEnabled = self.allowedParticipantRights.contains(.addMembers) case .pinMessages: - if defaultBannedFlags.contains(.banPinMessages) { - continue loop - } - - itemTitle = "Pin Messages" - itemValue = self.configPinMessages + isEnabled = self.allowedParticipantRights.contains(.pinMessages) case .changeInfo: - if defaultBannedFlags.contains(.banChangeInfo) { - continue loop - } - - itemTitle = "Change Chat Info" - itemValue = self.configChangeInfo + isEnabled = self.allowedParticipantRights.contains(.changeInfo) } - - configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + allConfigItems.append((configItem, isEnabled)) + } + } + + loop: for (configItem, isEnabled) in allConfigItems { + let itemTitle: AnyComponent + let itemValue: Bool + switch configItem { + case .sendMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Send Messages", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.sendMessages) + case .sendMedia: + if isEnabled { + itemTitle = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: itemTitle, + string: "Send Media", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle( - style: .icons, - isOn: itemValue, - isInteractive: true, + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: environment.theme, + title: "\(self.mediaRights.count)/\(self.allowedMediaRights.count)", + isExpanded: self.isMediaSectionExpanded + ))) + ], spacing: 7.0)) + } else { + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Send Media", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + } + + itemValue = !self.mediaRights.isEmpty + case .addUsers: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Add Users", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.addMembers) + case .pinMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Pin Messages", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.pinMessages) + case .changeInfo: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Change Chat Info", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.changeInfo) + } + + configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: itemTitle, + accessory: .toggle(ListActionItemComponent.Toggle( + style: isEnabled ? .icons : .lock, + isOn: itemValue, + isInteractive: isEnabled, + action: isEnabled ? { [weak self] _ in + guard let self else { + return + } + switch configItem { + case .sendMessages: + if self.participantRights.contains(.sendMessages) { + self.participantRights.remove(.sendMessages) + } else { + self.participantRights.insert(.sendMessages) + } + case .sendMedia: + if self.mediaRights.isEmpty { + self.mediaRights = self.allowedMediaRights + } else { + self.mediaRights = [] + } + case .addUsers: + if self.participantRights.contains(.addMembers) { + self.participantRights.remove(.addMembers) + } else { + self.participantRights.insert(.addMembers) + } + case .pinMessages: + if self.participantRights.contains(.pinMessages) { + self.participantRights.remove(.pinMessages) + } else { + self.participantRights.insert(.pinMessages) + } + case .changeInfo: + if self.participantRights.contains(.changeInfo) { + self.participantRights.remove(.changeInfo) + } else { + self.participantRights.insert(.changeInfo) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + } : nil + )), + action: (isEnabled && configItem == .sendMedia) ? { [weak self] _ in + guard let self else { + return + } + self.isMediaSectionExpanded = !self.isMediaSectionExpanded + self.state?.updated(transition: .spring(duration: 0.35)) + } : nil, + highlighting: .disabled + )))) + + if isEnabled, case .sendMedia = configItem, self.isMediaSectionExpanded { + var mediaItems: [AnyComponentWithIdentity] = [] + mediaRightsLoop: for possibleMediaItem in allMediaRightItems { + if !self.allowedMediaRights.contains(possibleMediaItem) { + continue + } + + let mediaItemTitle: String + switch possibleMediaItem { + case .photos: + mediaItemTitle = "Send Photos" + case .videos: + mediaItemTitle = "Send Videos" + case .stickersAndGifs: + mediaItemTitle = "Send Stickers & GIFs" + case .music: + mediaItemTitle = "Send Music" + case .files: + mediaItemTitle = "Send Files" + case .voiceMessages: + mediaItemTitle = "Send Voice Messages" + case .videoMessages: + mediaItemTitle = "Send Video Messages" + case .links: + mediaItemTitle = "Embed Links" + case .polls: + mediaItemTitle = "Send Polls" + default: + continue mediaRightsLoop + } + + mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: mediaItemTitle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: self.mediaRights.contains(possibleMediaItem), + toggle: { [weak self] in + guard let self else { + return + } + + if self.mediaRights.contains(possibleMediaItem) { + self.mediaRights.remove(possibleMediaItem) + } else { + self.mediaRights.insert(possibleMediaItem) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + icon: .none, + accessory: .none, action: { [weak self] _ in guard let self else { return } - switch configItem { - case .sendMessages: - self.configSendMessages = !self.configSendMessages - case .sendMedia: - self.configSendMedia = !self.configSendMedia - case .addUsers: - self.configAddUsers = !self.configAddUsers - case .pinMessages: - self.configPinMessages = !self.configPinMessages - case .changeInfo: - self.configChangeInfo = !self.configChangeInfo + + if self.mediaRights.contains(possibleMediaItem) { + self.mediaRights.remove(possibleMediaItem) + } else { + self.mediaRights.insert(possibleMediaItem) } + self.state?.updated(transition: .spring(duration: 0.35)) - } - )), - action: nil + }, + highlighting: .disabled + )))) + } + configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 0.0, + items: mediaItems )))) } } @@ -911,23 +1274,35 @@ private final class AdminUserActionsSheetComponent: Component { transition.setAlpha(view: configSectionView, alpha: self.isConfigurationExpanded ? 1.0 : 0.0) } - let optionsFooterFrame: CGRect - if self.isConfigurationExpanded { - contentHeight += 30.0 - contentHeight += configSectionSize.height - contentHeight += 7.0 - optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) - contentHeight += optionsFooterSize.height - } else { - contentHeight += 7.0 - optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) - contentHeight += optionsFooterSize.height - } - if let optionsFooterView = self.optionsFooter.view { - if optionsFooterView.superview == nil { - self.scrollContentView.addSubview(optionsFooterView) + if availableOptions.contains(.ban) && !configSectionItems.isEmpty { + let optionsFooterFrame: CGRect + if self.isConfigurationExpanded { + contentHeight += 30.0 + contentHeight += configSectionSize.height + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } else { + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.scrollContentView.addSubview(optionsFooterView) + } + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 1.0) + } + } else { + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.scrollContentView.addSubview(optionsFooterView) + } + let optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 0.0) } - transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) } contentHeight += 30.0 @@ -1034,7 +1409,7 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { public let banPeers: [EnginePeer.Id] public let updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] - init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id : TelegramChatBannedRights]) { + init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]) { self.reportSpamPeers = reportSpamPeers self.deleteAllFromPeers = deleteAllFromPeers self.banPeers = banPeers @@ -1046,47 +1421,9 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, chatPeer: EnginePeer, peers: [EnginePeer], messageCount: Int, completion: @escaping (Result) -> Void) { + public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (Result) -> Void) { self.context = context - /*#if DEBUG - var peers = peers - - if !"".isEmpty { - var nextPeerId: Int64 = 1 - let makePeer: () -> EnginePeer = { - guard case let .user(user) = peers[0] else { - preconditionFailure() - } - let id = nextPeerId - nextPeerId += 1 - return .user(TelegramUser( - id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)), - accessHash: user.accessHash, - firstName: user.firstName, - lastName: user.lastName, - username: user.username, - phone: user.phone, - photo: user.photo, - botInfo: user.botInfo, - restrictionInfo: user.restrictionInfo, - flags: user.flags, - emojiStatus: user.emojiStatus, - usernames: user.usernames, - storiesHidden: user.storiesHidden, - nameColor: user.nameColor, - backgroundEmojiId: user.backgroundEmojiId, - profileColor: user.profileColor, - profileBackgroundEmojiId: user.profileBackgroundEmojiId - )) - } - - for _ in 0 ..< 3 { - peers.append(makePeer()) - } - } - #endif*/ - super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore @@ -1128,7 +1465,7 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { } private let optionExpandUsersIcon: UIImage? = { - let sourceImage = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")! + let sourceImage = UIImage(bundleImageName: "Item List/InlineIconUsers")! return generateImage(CGSize(width: sourceImage.size.width, height: sourceImage.size.height), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) @@ -1185,21 +1522,21 @@ private final class OptionSectionExpandIndicatorComponent: Component { } func update(component: OptionSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let countArrowSpacing: CGFloat = -1.0 - let iconCountSpacing: CGFloat = 2.0 + let countArrowSpacing: CGFloat = 1.0 + let iconCountSpacing: CGFloat = 1.0 if self.iconView.image == nil { self.iconView.image = optionExpandUsersIcon } self.iconView.tintColor = component.theme.list.itemPrimaryTextColor - let iconSize = CGSize(width: 12.0, height: 12.0) + let iconSize = self.iconView.image?.size ?? CGSize(width: 12.0, height: 12.0) if self.arrowView.image == nil { - self.arrowView.image = PresentationResourcesItemList.downArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate) + self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) } self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor - - let arrowSize = CGSize(width: 20.0, height: 20.0) + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + let countSize = self.count.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -1211,12 +1548,7 @@ private final class OptionSectionExpandIndicatorComponent: Component { let size = CGSize(width: 60.0, height: availableSize.height) - var arrowFrame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 10.0, y: floor((size.height - arrowSize.height) * 0.5)), size: arrowSize) - if component.isExpanded { - arrowFrame = arrowFrame.offsetBy(dx: 0.0, dy: -1.0) - } else { - arrowFrame = arrowFrame.offsetBy(dx: 0.0, dy: 1.0) - } + let arrowFrame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 12.0, y: floor((size.height - arrowSize.height) * 0.5)), size: arrowSize) let countFrame = CGRect(origin: CGPoint(x: arrowFrame.minX - countArrowSpacing - countSize.width, y: floor((size.height - countSize.height) * 0.5)), size: countSize) @@ -1229,9 +1561,9 @@ private final class OptionSectionExpandIndicatorComponent: Component { countView.frame = countFrame } - transition.setPosition(view: self.arrowView, position: arrowFrame.center) + self.arrowView.center = arrowFrame.center self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) - transition.setTransform(view: self.arrowView, transform: CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0)) + transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : 0.0, 0.0)) self.iconView.frame = iconFrame @@ -1248,6 +1580,97 @@ private final class OptionSectionExpandIndicatorComponent: Component { } } +private final class MediaSectionExpandIndicatorComponent: Component { + let theme: PresentationTheme + let title: String + let isExpanded: Bool + + init( + theme: PresentationTheme, + title: String, + isExpanded: Bool + ) { + self.theme = theme + self.title = title + self.isExpanded = isExpanded + } + + static func ==(lhs: MediaSectionExpandIndicatorComponent, rhs: MediaSectionExpandIndicatorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private let arrowView: UIImageView + private let title = ComponentView() + + override init(frame: CGRect) { + self.arrowView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.arrowView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let titleArrowSpacing: CGFloat = 1.0 + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) + } + self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: titleSize.width + titleArrowSpacing + arrowSize.width, height: titleSize.height) + + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let arrowFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleArrowSpacing, y: floor((size.height - arrowSize.height) * 0.5) + 2.0), size: arrowSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + self.arrowView.center = arrowFrame.center + self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) + transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : -1.0, 0.0)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + private final class OptionsSectionFooterComponent: Component { let theme: PresentationTheme let text: String @@ -1304,11 +1727,10 @@ private final class OptionsSectionFooterComponent: Component { func update(component: OptionsSectionFooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { if self.arrowView.image == nil { - self.arrowView.image = PresentationResourcesItemList.downArrowImage(component.theme)?.withRenderingMode(.alwaysTemplate) + self.arrowView.image = PresentationResourcesItemList.expandSmallDownArrowImage(component.theme) } self.arrowView.tintColor = component.theme.list.itemAccentColor - - let arrowSize = CGSize(width: 14.0, height: 14.0) + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: component.text, font: Font.regular(component.fontSize), textColor: component.theme.list.itemAccentColor)) attributedText.append(NSAttributedString(string: ">", font: Font.regular(component.fontSize), textColor: .clear)) @@ -1321,7 +1743,7 @@ private final class OptionsSectionFooterComponent: Component { var arrowFrame = CGRect() if let lineRect = textLayout.linesRects().last { - arrowFrame = CGRect(origin: CGPoint(x: textFrame.minX + lineRect.maxX - arrowSize.width + 6.0, y: textFrame.minY + lineRect.maxY - lineRect.height - arrowSize.height + 4.0), size: arrowSize) + arrowFrame = CGRect(origin: CGPoint(x: textFrame.minX + lineRect.maxX - arrowSize.width + 6.0, y: textFrame.minY + lineRect.maxY - lineRect.height - arrowSize.height - 1.0), size: arrowSize) } self.arrowView.center = arrowFrame.center diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 754ab9d7af..b9647619b7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -403,7 +403,7 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { if let current = node.avatarNode { avatarNode = current } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0)) + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) node.avatarNode = avatarNode node.addSubnode(avatarNode) } diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 714c81efed..11620ebcf9 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -607,6 +607,7 @@ public final class ListActionItemComponent: Component { } } } + switchNode.isUserInteractionEnabled = toggle.isInteractive if updateSwitchTheme { @@ -624,11 +625,15 @@ public final class ListActionItemComponent: Component { var updateSwitchTheme = themeUpdated if let current = self.iconSwitchNode { switchNode = current - switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + switchNode.updateIsLocked(toggle.style == .lock) + if switchNode.isOn != toggle.isOn { + switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + } } else { switchTransition = switchTransition.withAnimation(.none) updateSwitchTheme = true switchNode = IconSwitchNode() + switchNode.updateIsLocked(toggle.style == .lock) switchNode.setOn(toggle.isOn, animated: false) self.iconSwitchNode = switchNode self.addSubview(switchNode.view) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index de8ddd556b..f0311c2cd3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -7000,7 +7000,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } UIPasteboard.general.string = bioText - self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: self.presentationData.strings.MyProfile_ToastBioCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + let toastText: String + if let _ = self.data?.peer as? TelegramUser { + toastText = self.presentationData.strings.MyProfile_ToastBioCopied + } else { + toastText = self.presentationData.strings.ChannelProfile_ToastAboutCopied + } + + self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: toastText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } var items: [ContextMenuItem] = [] @@ -7027,7 +7034,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.MyProfile_BioActionCopy, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + let copyText: String + if let _ = self.data?.peer as? TelegramUser { + copyText = self.presentationData.strings.MyProfile_BioActionCopy + } else { + copyText = self.presentationData.strings.ChannelProfile_AboutActionCopy + } + items.append(.action(ContextMenuActionItem(text: copyText, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c.dismiss { copyAction() } diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json new file mode 100644 index 0000000000..ccce3ad789 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delmsgarrow_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf new file mode 100644 index 0000000000..f0fb36302f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.000000 -1.000000 1.000000 0.000000 6.177734 10.500000 cm +0.000000 0.000000 0.000000 scn +0.586899 7.409164 m +0.262763 7.733299 -0.262763 7.733299 -0.586899 7.409164 c +-0.911034 7.085029 -0.911034 6.559502 -0.586899 6.235367 c +0.586899 7.409164 l +h +5.000000 1.822266 m +5.586899 1.235367 l +5.911034 1.559502 5.911034 2.085029 5.586899 2.409164 c +5.000000 1.822266 l +h +-0.586899 -2.590836 m +-0.911034 -2.914971 -0.911034 -3.440497 -0.586899 -3.764633 c +-0.262763 -4.088768 0.262763 -4.088768 0.586899 -3.764633 c +-0.586899 -2.590836 l +h +-0.586899 6.235367 m +4.413101 1.235367 l +5.586899 2.409164 l +0.586899 7.409164 l +-0.586899 6.235367 l +h +4.413101 2.409164 m +-0.586899 -2.590836 l +0.586899 -3.764633 l +5.586899 1.235367 l +4.413101 2.409164 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 780 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.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 + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000870 00000 n +0000000892 00000 n +0000001065 00000 n +0000001139 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1198 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json new file mode 100644 index 0000000000..7d40464891 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf new file mode 100644 index 0000000000..1cf0950fbb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.000000 -1.000000 1.000000 0.000000 3.682617 4.500000 cm +0.000000 0.000000 0.000000 scn +0.424264 5.241647 m +0.189949 5.475962 -0.189949 5.475962 -0.424264 5.241647 c +-0.658579 5.007332 -0.658579 4.627433 -0.424264 4.393119 c +0.424264 5.241647 l +h +3.500000 1.317383 m +3.924264 0.893119 l +4.158579 1.127433 4.158579 1.507332 3.924264 1.741647 c +3.500000 1.317383 l +h +-0.424264 -1.758353 m +-0.658579 -1.992668 -0.658579 -2.372567 -0.424264 -2.606881 c +-0.189949 -2.841196 0.189949 -2.841196 0.424264 -2.606881 c +-0.424264 -1.758353 l +h +-0.424264 4.393119 m +3.075736 0.893119 l +3.924264 1.741647 l +0.424264 5.241647 l +-0.424264 4.393119 l +h +3.075736 1.741647 m +-0.424264 -1.758353 l +0.424264 -2.606881 l +3.924264 0.893119 l +3.075736 1.741647 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 779 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 10.000000 6.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 + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000869 00000 n +0000000891 00000 n +0000001063 00000 n +0000001137 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1196 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json new file mode 100644 index 0000000000..2b79531409 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delmsgusers_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf new file mode 100644 index 0000000000..e42ada8d59 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf @@ -0,0 +1,98 @@ +%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 1.385620 3.388916 cm +0.000000 0.000000 0.000000 scn +4.505642 6.066639 m +5.622485 6.066639 6.527864 6.972019 6.527864 8.088861 c +6.527864 9.205704 5.622485 10.111084 4.505642 10.111084 c +3.388799 10.111084 2.483420 9.205704 2.483420 8.088861 c +2.483420 6.972019 3.388799 6.066639 4.505642 6.066639 c +h +9.128903 6.066618 m +10.086196 6.066618 10.862236 6.842658 10.862236 7.799952 c +10.862236 8.757245 10.086196 9.533284 9.128903 9.533284 c +8.171610 9.533284 7.395570 8.757245 7.395570 7.799952 c +7.395570 6.842658 8.171610 6.066618 9.128903 6.066618 c +h +8.084225 3.303497 m +8.414125 2.965264 8.662351 2.592029 8.849121 2.221855 c +9.037642 1.848210 9.052315 1.480731 8.941890 1.155545 c +8.715535 0.488963 7.963518 0.000092 7.105636 0.000092 c +1.905633 0.000092 l +0.629242 0.000092 -0.412813 1.082295 0.162149 2.221854 c +0.759815 3.406412 1.986777 4.622314 4.505635 4.622314 c +5.505974 4.622314 6.302553 4.430541 6.936879 4.125120 c +6.951430 4.118114 6.965898 4.111050 6.980279 4.103925 c +7.194489 3.997796 7.389868 3.878607 7.568068 3.749475 c +7.759200 3.610973 7.930571 3.461031 8.084225 3.303497 c +h +7.816253 4.510825 m +8.649220 3.970058 9.188520 3.255078 9.535180 2.568007 c +9.778294 2.086163 9.829369 1.599931 9.736712 1.155545 c +11.150474 1.155545 l +12.107767 1.155545 12.888267 1.965531 12.464911 2.824125 c +12.013588 3.739441 11.076003 4.690186 9.128251 4.690186 c +8.623553 4.690186 8.186680 4.626350 7.808517 4.515837 c +7.816253 4.510825 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1521 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.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 + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001611 00000 n +0000001634 00000 n +0000001807 00000 n +0000001881 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1940 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json new file mode 100644 index 0000000000..c6b56e0023 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf new file mode 100644 index 0000000000..db9260ea73 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf @@ -0,0 +1,91 @@ +%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.904785 0.982666 cm +0.000000 0.000000 0.000000 scn +6.094680 16.137451 m +3.493683 16.137451 1.385156 14.028931 1.385156 11.427931 c +1.385156 8.649511 l +0.873086 8.316221 0.469098 7.833959 0.231986 7.261518 c +0.000000 6.701453 0.000000 5.991447 0.000000 4.571436 c +0.000000 3.151424 0.000000 2.441419 0.231986 1.881354 c +0.541301 1.134602 1.134594 0.541308 1.881347 0.231994 c +2.441411 0.000008 3.151414 0.000008 4.571422 0.000008 c +7.619050 0.000008 l +9.039061 0.000008 9.749065 0.000008 10.309130 0.231994 c +11.055882 0.541308 11.649176 1.134602 11.958490 1.881354 c +12.190476 2.441419 12.190476 3.151424 12.190476 4.571436 c +12.190476 5.991447 12.190476 6.701453 11.958490 7.261518 c +11.721206 7.834374 11.316802 8.316923 10.804203 8.650238 c +10.804203 11.427927 l +10.804203 14.028925 8.695679 16.137451 6.094680 16.137451 c +h +9.004204 9.128557 m +9.004204 11.427927 l +9.004204 13.034813 7.701565 14.337451 6.094680 14.337451 c +4.487793 14.337451 3.185156 13.034816 3.185156 11.427931 c +3.185156 9.128514 l +3.559292 9.142864 4.009834 9.142864 4.571427 9.142864 c +7.619054 9.142864 l +8.180087 9.142864 8.630290 9.142864 9.004204 9.128557 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1216 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 14.000000 18.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 + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001306 00000 n +0000001329 00000 n +0000001502 00000 n +0000001576 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1635 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 22ec38f6b9..ebb2d8942d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -26,7 +26,7 @@ extension ChatControllerImpl { } //TODO:localize - let title: String = "Messages Deleted" + var title: String? = messageIds.count == 1 ? "Message Deleted" : "Messages Deleted" var text: String = "" var undoRights: [EnginePeer.Id: InitialBannedRights] = [:] @@ -35,9 +35,9 @@ extension ChatControllerImpl { text.append("\n") } if result.reportSpamPeers.count == 1 { - text.append("**1** user reported for spam") + text.append("**1** user reported for spam.") } else { - text.append("**\(result.reportSpamPeers.count)** users reported for spam") + text.append("**\(result.reportSpamPeers.count)** users reported for spam.") } } if !result.banPeers.isEmpty { @@ -45,9 +45,9 @@ extension ChatControllerImpl { text.append("\n") } if result.banPeers.count == 1 { - text.append("**1** user banned") + text.append("**1** user banned.") } else { - text.append("**\(result.banPeers.count)** users banned") + text.append("**\(result.banPeers.count)** users banned.") } for id in result.banPeers { if let value = initialUserBannedRights[id] { @@ -60,9 +60,9 @@ extension ChatControllerImpl { text.append("\n") } if result.updateBannedRights.count == 1 { - text.append("**1** user restricted") + text.append("**1** user restricted.") } else { - text.append("**\(result.updateBannedRights.count)** users restricted") + text.append("**\(result.updateBannedRights.count)** users restricted.") } for id in result.banPeers { if let value = initialUserBannedRights[id] { @@ -92,10 +92,15 @@ extension ChatControllerImpl { } } + if text.isEmpty { + text = messageIds.count == 1 ? "Message Deleted." : "Messages Deleted." + title = nil + } + self.present( UndoOverlayController( presentationData: self.presentationData, - content: undoRights.isEmpty ? .actionSucceeded(title: text.isEmpty ? nil : title, text: text.isEmpty ? title : text, cancel: nil, destructive: false) : .removedChat(title: title, text: text), + content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(title: title ?? text, text: title == nil ? nil : text), elevatedLayout: false, action: { [weak self] action in guard let self else { @@ -140,12 +145,19 @@ extension ChatControllerImpl { guard let self, let chatPeer else { return } - var peers: [EnginePeer] = [] + var renderedParticipants: [RenderedChannelParticipant] = [] var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] for maybeParticipant in participants { guard let participant = maybeParticipant else { continue } + guard let peer = authors.first(where: { $0.id == participant.peerId }) else { + continue + } + renderedParticipants.append(RenderedChannelParticipant( + participant: participant, + peer: peer + )) switch participant { case .creator: break @@ -157,13 +169,10 @@ extension ChatControllerImpl { } } } - for author in authors { - peers.append(EnginePeer(author)) - } self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, - peers: peers, + peers: renderedParticipants, messageCount: messageIds.count, completion: { [weak self] result in guard let self else { @@ -186,6 +195,9 @@ extension ChatControllerImpl { |> deliverOnMainQueue).startStrict(next: { [weak self] participant in if let strongSelf = self { if "".isEmpty { + guard let participant else { + return + } let _ = (strongSelf.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.Peer(id: author.id) @@ -198,22 +210,23 @@ extension ChatControllerImpl { return } var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] - if let participant { - switch participant { - case .creator: - break - case let .member(_, _, _, banInfo, _): - if let banInfo { - initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) - } else { - initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) - } + switch participant { + case .creator: + break + case let .member(_, _, _, banInfo, _): + if let banInfo { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) + } else { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) } } self.push(AdminUserActionsSheet( context: self.context, chatPeer: chatPeer, - peers: [authorPeer], + peers: [RenderedChannelParticipant( + participant: participant, + peer: authorPeer._asPeer() + )], messageCount: messageIds.count, completion: { [weak self] result in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 2f3c2d5e75..4a559c44a4 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -2182,7 +2182,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer if message.flags.contains(.Incoming) { optionsMap[id]!.insert(.report) } - if channel.hasPermission(.banMembers), case .group = channel.info { + if (channel.hasPermission(.banMembers) || channel.hasPermission(.deleteAllMessages)), case .group = channel.info { if message.flags.contains(.Incoming) { if let author = message.author { if author is TelegramUser { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 24b963a7ba..dce4053d8e 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1501,10 +1501,18 @@ public final class WebAppController: ViewController, AttachmentContainable { var alertTitle: String? let alertText: String if let reason { - alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + if case .touchId = LocalAuth.biometricAuthentication { + alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessTouchIDText(botPeer.compactDisplayTitle).string + } else { + alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + } alertText = reason } else { - alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + if case .touchId = LocalAuth.biometricAuthentication { + alertText = self.presentationData.strings.WebApp_AlertBiometryAccessTouchIDText(botPeer.compactDisplayTitle).string + } else { + alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + } } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: alertTitle, text: alertText, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: {