Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,387 @@
import Foundation
import UIKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import TelegramBaseController
import AccountContext
import AlertUI
import PresentationDataUtils
import ChatPresentationInterfaceState
import ChatNavigationButton
import CounterControllerTitleView
import AdminUserActionsSheet
public final class ChatRecentActionsController: TelegramBaseController {
private var controllerNode: ChatRecentActionsControllerNode {
return self.displayNode as! ChatRecentActionsControllerNode
}
private let context: AccountContext
private let peer: Peer
private let initialAdminPeerId: PeerId?
let starsState: StarsRevenueStats?
private var presentationData: PresentationData
private var presentationDataPromise = Promise<PresentationData>()
override public var updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get())
}
private var presentationDataDisposable: Disposable?
private var didSetPresentationData = false
private var panelInteraction: ChatPanelInterfaceInteraction!
private let titleView: CounterControllerTitleView
private var rightBarButton: ChatNavigationButton?
private var adminsDisposable: Disposable?
public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) {
self.context = context
self.peer = peer
self.initialAdminPeerId = adminPeerId
self.starsState = starsState
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.titleView = CounterControllerTitleView(theme: self.presentationData.theme)
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none)
self.automaticallyControlPresentationContextLayout = false
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.panelInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _, _ in
}, setupEditMessage: { _, _ in
}, beginMessageSelection: { _, _ in
}, cancelMessageSelection: { _ in
}, deleteSelectedMessages: {
}, reportSelectedMessages: {
}, reportMessages: { _, _ in
}, blockMessageAuthor: { _, _ in
}, deleteMessages: { _, _, f in
f(.default)
}, forwardSelectedMessages: {
}, forwardCurrentForwardMessages: {
}, forwardMessages: { _ in
}, updateForwardOptionsState: { _ in
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
}, presentLinkOptions: { _ in
}, presentSuggestPostOptions: {
}, shareSelectedMessages: {
}, updateTextInputStateAndMode: { _ in
}, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in
}, openStickers: {
}, editMessage: {
}, beginMessageSearch: { _, _ in
}, dismissMessageSearch: {
}, updateMessageSearch: { _ in
}, openSearchResults: {
}, navigateMessageSearch: { _ in
}, openCalendarSearch: {
}, toggleMembersSearch: { _ in
}, navigateToMessage: { _, _, _, _ in
}, navigateToChat: { _ in
}, navigateToProfile: { _ in
}, openPeerInfo: {
}, togglePeerNotifications: {
}, sendContextResult: { _, _, _, _ in
return false
}, sendBotCommand: { _, _ in
}, sendShortcut: { _ in
}, openEditShortcuts: {
}, sendBotStart: { _ in
}, botSwitchChatWithPayload: { _, _ in
}, beginMediaRecording: { _ in
}, finishMediaRecording: { _ in
}, stopMediaRecording: {
}, lockMediaRecording: {
}, resumeMediaRecording: {
}, deleteRecordedMedia: {
}, sendRecordedMedia: { _, _ in
}, displayRestrictedInfo: { _, _ in
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
}, sendSticker: { _, _, _, _, _, _ in
return false
}, unblockPeer: {
}, pinMessage: { _, _ in
}, unpinMessage: { _, _, _ in
}, unpinAllMessages: {
}, openPinnedList: { _ in
}, shareAccountContact: {
}, reportPeer: {
}, presentPeerContact: {
}, dismissReportPeer: {
}, deleteChat: {
}, beginCall: { _ in
}, toggleMessageStickerStarred: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, getNavigationController: {
return nil
}, presentGlobalOverlayController: { _, _ in
}, navigateFeed: {
}, openGrouping: {
}, toggleSilentPost: {
}, requestUnvoteInMessage: { _ in
}, requestStopPollInMessage: { _ in
}, updateInputLanguage: { _ in
}, unarchiveChat: {
}, openLinkEditing: {
}, displaySlowmodeTooltip: { _, _ in
}, displaySendMessageOptions: { _, _ in
}, openScheduledMessages: {
}, openPeersNearby: {
}, displaySearchResultsTooltip: { _, _ in
}, unarchivePeer: {
}, scrollToTop: {
}, viewReplies: { _, _ in
}, activatePinnedListPreview: { _, _ in
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openMonoforum: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in
}, openInviteRequests: {
}, openSendAsPeer: { _, _ in
}, presentChatRequestAdminInfo: {
}, displayCopyProtectionTip: { _, _ in
}, openWebView: { _, _, _, _ in
}, updateShowWebView: { _ in
}, insertText: { _ in
}, backwardsDeleteText: {
}, restartTopic: {
}, toggleTranslation: { _ in
}, changeTranslationLanguage: { _ in
}, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: {
}, openPremiumGift: {
}, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in
}, openMessagePayment: {
}, openBoostToUnrestrict: {
}, updateRecordingTrimRange: { _, _, _, _ in
}, dismissAllTooltips: {
}, editTodoMessage: { _, _, _ in
}, dismissUrlPreview: {
}, dismissForwardMessages: {
}, dismissSuggestPost: {
}, displayUndo: { _ in
}, sendEmoji: { _, _, _ in
}, updateHistoryFilter: { _ in
}, updateChatLocationThread: { _, _ in
}, toggleChatSidebarMode: {
}, updateDisplayHistoryFilterAsList: { _ in
}, requestLayout: { _ in
}, chatController: {
return nil
}, statuses: nil)
self.navigationItem.titleView = self.titleView
let rightBarButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
self.rightBarButton = rightBarButton
self.titleView.title = CounterControllerTitle(title: EnginePeer(peer).compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents)
let chatTheme = self.context.account.postbox.peerView(id: peer.id)
|> map { view -> ChatTheme? in
let cachedData = view.cachedData
if let cachedData = cachedData as? CachedUserData {
return cachedData.chatTheme
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.chatTheme
} else if let cachedData = cachedData as? CachedChannelData {
return cachedData.chatTheme
} else {
return nil
}
}
|> distinctUntilChanged
self.presentationDataDisposable = combineLatest(
queue: Queue.mainQueue(),
context.sharedContext.presentationData,
context.engine.themes.getChatThemes(accountManager: context.sharedContext.accountManager, onlyCached: true),
chatTheme
).startStrict(next: { [weak self] presentationData, chatThemes, chatTheme in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
var presentationData = presentationData
if let chatTheme {
switch chatTheme {
case let .emoticon(emoticon):
if let theme = chatThemes.first(where: { $0.emoticon == emoticon }), let theme = makePresentationTheme(cloudTheme: theme, dark: presentationData.theme.overallDarkAppearance) {
presentationData = presentationData.withUpdated(theme: theme)
presentationData = presentationData.withUpdated(chatWallpaper: theme.chat.defaultWallpaper)
}
case let .gift(gift, wallpaper):
let _ = gift
let _ = wallpaper
//TODO:release
}
}
let isFirstTime = !strongSelf.didSetPresentationData
strongSelf.presentationData = presentationData
strongSelf.presentationDataPromise.set(.single(presentationData))
strongSelf.didSetPresentationData = true
if isFirstTime || previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings()
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.adminsDisposable?.dispose()
}
private func updateThemeAndStrings() {
self.titleView.theme = self.presentationData.theme
self.updateTitle()
let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.controllerNode.updatePresentationData(self.presentationData)
}
override public func loadDisplayNode() {
self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in
(self?.navigationController as? NavigationController)?.pushViewController(c)
}, presentController: { [weak self] c, t, a in
self?.present(c, in: t, with: a, blockInteraction: true)
}, getNavigationController: { [weak self] in
return self?.navigationController as? NavigationController
})
self.controllerNode.isEmptyUpdated = { [weak self] isEmpty in
guard let self, let rightBarButton = self.rightBarButton else {
return
}
self.navigationItem.setRightBarButton(isEmpty ? nil : rightBarButton.buttonItem, animated: true)
}
if let adminPeerId = self.initialAdminPeerId {
self.controllerNode.updateFilter(events: .all, adminPeerIds: [adminPeerId])
self.updateTitle()
}
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
var childrenLayout = layout
childrenLayout.intrinsicInsets.bottom += 49.0
self.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func activateSearch() {
if let navigationBar = self.navigationBar {
if !(navigationBar.contentNode is ChatRecentActionsSearchNavigationContentNode) {
let searchNavigationNode = ChatRecentActionsSearchNavigationContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, cancel: { [weak self] in
self?.deactivateSearch()
})
navigationBar.setContentNode(searchNavigationNode, animated: true)
searchNavigationNode.setQueryUpdated({ [weak self] query in
self?.controllerNode.updateSearchQuery(query)
self?.updateTitle()
})
searchNavigationNode.activate()
}
}
}
private func deactivateSearch() {
self.controllerNode.updateSearchQuery("")
self.navigationBar?.setContentNode(nil, animated: true)
self.updateTitle()
}
private var adminsPromise: Promise<[RenderedChannelParticipant]?>?
func openFilterSetup() {
if self.adminsPromise == nil {
self.adminsPromise = Promise()
let (disposable, _) = self.context.peerChannelMemberCategoriesContextsManager.admins(engine: self.context.engine, postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: self.context.account.peerId, peerId: self.peer.id) { membersState in
if case .loading = membersState.loadingState, membersState.list.isEmpty {
self.adminsPromise?.set(.single(nil))
} else {
self.adminsPromise?.set(.single(membersState.list))
}
}
self.adminsDisposable = disposable
}
guard let adminsPromise = self.adminsPromise else {
return
}
let _ = (adminsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
var adminPeers: [EnginePeer] = []
if let result {
for participant in result {
adminPeers.append(EnginePeer(participant.peer))
}
}
let controller = RecentActionsSettingsSheet(
context: self.context,
peer: EnginePeer(self.peer),
adminPeers: adminPeers,
initialValue: RecentActionsSettingsSheet.Value(
events: self.controllerNode.filter.events,
admins: self.controllerNode.filter.adminPeerIds
),
completion: { [weak self] result in
guard let self else {
return
}
self.controllerNode.updateFilter(events: result.events, adminPeerIds: result.admins)
self.updateTitle()
}
)
self.push(controller)
})
}
private func updateTitle() {
let title = EnginePeer(self.peer).compactDisplayTitle
let subtitle: String
if self.controllerNode.filter.isEmpty {
subtitle = self.presentationData.strings.Channel_AdminLog_TitleAllEvents
} else {
subtitle = self.presentationData.strings.Channel_AdminLog_TitleSelectedEvents
}
self.titleView.title = CounterControllerTitle(title: title, counter: subtitle)
}
}
@@ -0,0 +1,159 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import WallpaperBackgroundNode
import ChatPresentationInterfaceState
private let titleFont = Font.semibold(15.0)
private let textFont = Font.regular(13.0)
public final class ChatRecentActionsEmptyNode: ASDisplayNode {
private var theme: PresentationTheme
private var chatWallpaper: TelegramWallpaper
private var hasIcon: Bool
private let backgroundNode: NavigationBackgroundNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let textNode: TextNode
private var wallpaperBackgroundNode: WallpaperBackgroundNode?
private var backgroundContent: WallpaperBubbleBackgroundNode?
private var absolutePosition: (CGRect, CGSize)?
private var layoutParams: (CGSize, ChatPresentationData)?
private var title: String = ""
private var text: String = ""
public init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, chatBubbleCorners: PresentationChatBubbleCorners, hasIcon: Bool) {
self.theme = theme
self.chatWallpaper = chatWallpaper
self.hasIcon = hasIcon
self.backgroundNode = NavigationBackgroundNode(color: .clear)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.allowsGroupOpacity = true
self.addSubnode(self.backgroundNode)
if hasIcon {
self.addSubnode(self.iconNode)
}
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
}
public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) {
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
}
public func updateLayout(presentationData: ChatPresentationData, backgroundNode: WallpaperBackgroundNode, size: CGSize, transition: ContainedViewLayoutTransition) {
self.wallpaperBackgroundNode = backgroundNode
self.layoutParams = (size, presentationData)
let themeUpdated = self.theme !== presentationData.theme.theme
self.theme = presentationData.theme.theme
self.chatWallpaper = presentationData.theme.wallpaper
self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate)
let insets = self.hasIcon ? UIEdgeInsets(top: 16.0, left: 16.0, bottom: 25.0, right: 16.0) : UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
let maxTextWidth = min(196.0, size.width - insets.left - insets.right - 18.0 * 2.0)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let serviceColor = serviceMessageColorComponents(theme: self.theme, wallpaper: self.chatWallpaper)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: titleFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let spacing: CGFloat = titleLayout.size.height.isZero ? 0.0 : 7.0
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let iconSize: CGSize
if self.hasIcon {
if themeUpdated || self.iconNode.image == nil {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Recent Actions/Placeholder"), color: serviceColor.primaryText)
}
iconSize = self.iconNode.image?.size ?? .zero
contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: 5.0 + insets.bottom + iconSize.height - 2.0 + titleLayout.size.height + spacing + textLayout.size.height)
} else {
iconSize = .zero
contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: insets.top + insets.bottom + titleLayout.size.height + spacing + textLayout.size.height)
}
let backgroundFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(14.0, self.backgroundNode.bounds.height / 2.0), transition: transition)
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - iconSize.width) / 2.0), y: backgroundFrame.minY + 5.0), size: iconSize)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - titleLayout.size.width) / 2.0), y: self.hasIcon ? iconFrame.maxY - 2.0 : backgroundFrame.minY + insets.top), size: titleLayout.size)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - textLayout.size.width) / 2.0), y: titleFrame.maxY + spacing), size: textLayout.size)
transition.updateFrame(node: self.textNode, frame: textFrame)
let _ = titleApply()
let _ = textApply()
if backgroundNode.hasExtraBubbleBackground() == true {
if self.backgroundContent == nil, let backgroundContent = backgroundNode.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
self.backgroundContent = backgroundContent
self.insertSubnode(backgroundContent, at: 0)
}
} else {
self.backgroundContent?.removeFromSupernode()
self.backgroundContent = nil
}
if let backgroundContent = self.backgroundContent {
self.backgroundNode.isHidden = true
backgroundContent.cornerRadius = 14.0
backgroundContent.frame = backgroundFrame
if let (rect, containerSize) = self.absolutePosition {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
} else {
self.backgroundNode.isHidden = false
}
}
public func setup(title: String, text: String) {
if self.title != title || self.text != text {
self.title = title
self.text = text
if let (size, presentationData) = self.layoutParams, let wallpaperBackgroundNode = self.wallpaperBackgroundNode {
self.updateLayout(presentationData: presentationData, backgroundNode: wallpaperBackgroundNode, size: size, transition: .immediate)
}
}
}
}
@@ -0,0 +1,504 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerItem
private final class ChatRecentActionsFilterControllerArguments {
let context: AccountContext
let toggleAllActions: (Bool) -> Void
let toggleAction: ([AdminLogEventsFlags]) -> Void
let toggleAllAdmins: (Bool) -> Void
let toggleAdmin: (PeerId) -> Void
init(context: AccountContext, toggleAllActions: @escaping (Bool) -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping (Bool) -> Void, toggleAdmin: @escaping (PeerId) -> Void) {
self.context = context
self.toggleAllActions = toggleAllActions
self.toggleAction = toggleAction
self.toggleAllAdmins = toggleAllAdmins
self.toggleAdmin = toggleAdmin
}
}
private enum ChatRecentActionsFilterSection: Int32 {
case actions
case admins
}
private enum ChatRecentActionsFilterEntryStableId: Hashable {
case index(Int32)
case peer(PeerId)
}
private enum ChatRecentActionsFilterEntry: ItemListNodeEntry {
case actionsTitle(PresentationTheme, String)
case allActions(PresentationTheme, String, Bool)
case actionItem(PresentationTheme, Int32, [AdminLogEventsFlags], String, Bool)
case adminsTitle(PresentationTheme, String)
case allAdmins(PresentationTheme, String, Bool)
case adminPeerItem(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Int32, RenderedChannelParticipant, Bool, Bool)
var section: ItemListSectionId {
switch self {
case .actionsTitle, .allActions, .actionItem:
return ChatRecentActionsFilterSection.actions.rawValue
case .adminsTitle, .allAdmins, .adminPeerItem:
return ChatRecentActionsFilterSection.admins.rawValue
}
}
var stableId: ChatRecentActionsFilterEntryStableId {
switch self {
case .actionsTitle:
return .index(0)
case .allActions:
return .index(1)
case let .actionItem(_, index, _, _, _):
return .index(100 + index)
case .adminsTitle:
return .index(200)
case .allAdmins:
return .index(201)
case let .adminPeerItem(_, _, _, _, _, participant, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool {
switch lhs {
case let .actionsTitle(lhsTheme, lhsText):
if case let .actionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .allActions(lhsTheme, lhsText, lhsValue):
if case let .allActions(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .actionItem(lhsTheme, lhsIndex, lhsFlags, lhsText, lhsValue):
if case let .actionItem(rhsTheme, rhsIndex, rhsFlags, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsFlags == rhsFlags, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .adminsTitle(lhsTheme, lhsText):
if case let .adminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .allAdmins(lhsTheme, lhsText, lhsValue):
if case let .allAdmins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .adminPeerItem(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsIndex, lhsParticipant, lhsIsAntiSpam, lhsChecked):
if case let .adminPeerItem(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsIndex, rhsParticipant, rhsIsAntiSpam, rhsChecked) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameDisplayOrder != rhsNameDisplayOrder {
return false
}
if lhsIndex != rhsIndex {
return false
}
if lhsParticipant != rhsParticipant {
return false
}
if lhsIsAntiSpam != rhsIsAntiSpam {
return false
}
if lhsChecked != rhsChecked {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool {
switch lhs {
case .actionsTitle:
return true
case .allActions:
switch rhs {
case .actionsTitle:
return false
default:
return true
}
case let .actionItem(_, lhsIndex, _, _, _):
switch rhs {
case .actionsTitle, .allActions:
return false
case let .actionItem(_, rhsIndex, _, _, _):
return lhsIndex < rhsIndex
default:
return true
}
case .adminsTitle:
switch rhs {
case .adminPeerItem, .allAdmins:
return true
default:
return false
}
case .allAdmins:
switch rhs {
case .adminPeerItem:
return true
default:
return false
}
case let .adminPeerItem(_, _, _, _, lhsIndex, _, _, _):
switch rhs {
case let .adminPeerItem(_, _, _, _, rhsIndex, _, _, _):
return lhsIndex < rhsIndex
default:
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChatRecentActionsFilterControllerArguments
switch self {
case let .actionsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .allActions(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleAllActions(value)
})
case let .actionItem(_, _, events, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleAction(events)
})
case let .adminsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .allAdmins(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleAllAdmins(value)
})
case let .adminPeerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, isAntiSpam, checked):
var peerText: String = ""
if isAntiSpam {
peerText = strings.Group_Management_AntiSpamMagic
} else {
switch participant.participant {
case .creator:
peerText = strings.Channel_Management_LabelOwner.lowercased()
case .member:
peerText = strings.ChatAdmins_AdminLabel.lowercased()
}
}
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: {
arguments.toggleAdmin(participant.peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in })
}
}
}
private struct ChatRecentActionsFilterControllerState: Equatable {
let events: AdminLogEventsFlags
let adminPeerIds: [PeerId]?
init(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) {
self.events = events
self.adminPeerIds = adminPeerIds
}
static func ==(lhs: ChatRecentActionsFilterControllerState, rhs: ChatRecentActionsFilterControllerState) -> Bool {
if lhs.events != rhs.events {
return false
}
if let lhsAdminPeerIds = lhs.adminPeerIds, let rhsAdminPeerIds = rhs.adminPeerIds {
if lhsAdminPeerIds != rhsAdminPeerIds {
return false
}
} else if (lhs.adminPeerIds != nil) != (rhs.adminPeerIds != nil) {
return false
}
return true
}
func withUpdatedEvents(_ events: AdminLogEventsFlags) -> ChatRecentActionsFilterControllerState {
return ChatRecentActionsFilterControllerState(events: events, adminPeerIds: self.adminPeerIds)
}
func withUpdatedAdminPeerIds(_ adminPeerIds: [PeerId]?) -> ChatRecentActionsFilterControllerState {
return ChatRecentActionsFilterControllerState(events: self.events, adminPeerIds: adminPeerIds)
}
}
private func channelRecentActionsFilterControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, peer: Peer, antiSpamBotId: PeerId?, state: ChatRecentActionsFilterControllerState, participants: [RenderedChannelParticipant]?) -> [ChatRecentActionsFilterEntry] {
var isGroup = true
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
}
var entries: [ChatRecentActionsFilterEntry] = []
let order: [([AdminLogEventsFlags], String)]
if isGroup {
order = [
([.ban, .unban, .kick, .unkick], presentationData.strings.Channel_AdminLogFilter_EventsRestrictions),
([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins),
([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers),
([.info, .settings], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo),
([.invites], presentationData.strings.Channel_AdminLogFilter_EventsInviteLinks),
([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages),
([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages),
([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned),
([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving),
([.calls], presentationData.strings.Channel_AdminLogFilter_EventsCalls)
]
} else {
order = [
([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins),
([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers),
([.info, .settings], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo),
([.invites], presentationData.strings.Channel_AdminLogFilter_EventsInviteLinks),
([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages),
([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages),
([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned),
([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving),
([.calls], presentationData.strings.Channel_AdminLogFilter_EventsLiveStreams)
]
}
var allTypesSelected = true
outer: for (events, _) in order {
for event in events {
if !state.events.contains(event) {
allTypesSelected = false
break outer
}
}
}
entries.append(.actionsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsTitle))
entries.append(.allActions(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsAll, allTypesSelected))
var index: Int32 = 0
for (events, text) in order {
var eventsSelected = true
inner: for event in events {
if !state.events.contains(event) {
eventsSelected = false
break inner
}
}
entries.append(.actionItem(presentationData.theme, index, events, text, eventsSelected))
index += 1
}
if let participants = participants {
var allAdminsSelected = true
if let adminPeerIds = state.adminPeerIds {
for participant in participants {
if !adminPeerIds.contains(participant.peer.id) {
allAdminsSelected = false
break
}
}
} else {
allAdminsSelected = true
}
entries.append(.adminsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsTitle))
entries.append(.allAdmins(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsAll, allAdminsSelected))
var index: Int32 = 0
for participant in participants {
var adminSelected = true
if let adminPeerIds = state.adminPeerIds {
if !adminPeerIds.contains(participant.peer.id) {
adminSelected = false
}
} else {
adminSelected = true
}
entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index, participant, participant.peer.id == antiSpamBotId, adminSelected))
index += 1
}
}
return entries
}
public func channelRecentActionsFilterController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: Peer, events: AdminLogEventsFlags, adminPeerIds: [PeerId]?, apply: @escaping (_ events: AdminLogEventsFlags, _ adminPeerIds: [PeerId]?) -> Void) -> ViewController {
let statePromise = ValuePromise(ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds), ignoreRepeated: true)
let stateValue = Atomic(value: ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds))
let updateState: ((ChatRecentActionsFilterControllerState) -> ChatRecentActionsFilterControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil)
let actionsDisposable = DisposableSet()
let arguments = ChatRecentActionsFilterControllerArguments(context: context, toggleAllActions: { value in
updateState { current in
if value {
return current.withUpdatedEvents(.all)
} else {
return current.withUpdatedEvents([])
}
}
}, toggleAction: { events in
if let first = events.first {
updateState { current in
var updatedEvents = current.events
if updatedEvents.contains(first) {
for event in events {
updatedEvents.remove(event)
}
} else {
for event in events {
updatedEvents.insert(event)
}
}
return current.withUpdatedEvents(updatedEvents)
}
}
}, toggleAllAdmins: { value in
let _ = (adminsPromise.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { admins in
if let _ = admins {
updateState { current in
if value {
return current.withUpdatedAdminPeerIds(nil)
} else {
return current.withUpdatedAdminPeerIds([])
}
}
}
})
}, toggleAdmin: { adminId in
let _ = (adminsPromise.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { admins in
if let admins = admins {
updateState { current in
if let adminPeerIds = current.adminPeerIds, let index = adminPeerIds.firstIndex(of: adminId) {
var updatedAdminPeerIds = adminPeerIds
updatedAdminPeerIds.remove(at: index)
return current.withUpdatedAdminPeerIds(updatedAdminPeerIds)
} else {
var updatedAdminPeerIds = current.adminPeerIds ?? admins.map { $0.peer.id }
if updatedAdminPeerIds.contains(adminId), let index = updatedAdminPeerIds.firstIndex(of: adminId) {
updatedAdminPeerIds.remove(at: index)
} else {
updatedAdminPeerIds.append(adminId)
}
return current.withUpdatedAdminPeerIds(updatedAdminPeerIds)
}
}
}
})
})
adminsPromise.set(.single(nil))
let (membersDisposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peer.id) { membersState in
if case .loading = membersState.loadingState, membersState.list.isEmpty {
adminsPromise.set(.single(nil))
} else {
adminsPromise.set(.single(membersState.list))
}
}
actionsDisposable.add(membersDisposable)
let antiSpamBotConfiguration = AntiSpamBotConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let antiSpamBotPeerPromise = Promise<RenderedChannelParticipant?>(nil)
if let antiSpamBotId = antiSpamBotConfiguration.antiSpamBotId {
antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId))
|> map { peer in
if let peer = peer, case let .user(user) = peer {
return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user)
} else {
return nil
}
})
}
var previousPeers: [RenderedChannelParticipant]?
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(presentationData, statePromise.get(), adminsPromise.get(), antiSpamBotPeerPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, admins, antiSpamBot -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let doneEnabled = !state.events.isEmpty
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var resultState: ChatRecentActionsFilterControllerState?
updateState { current in
resultState = current
return current
}
if let resultState = resultState {
apply(resultState.events, resultState.adminPeerIds)
}
dismissImpl?()
})
var sortedAdmins: [RenderedChannelParticipant]?
if let admins = admins {
sortedAdmins = admins.filter { $0.peer.id == context.account.peerId } + admins.filter({ $0.peer.id != context.account.peerId })
if let antiSpamBot = antiSpamBot {
sortedAdmins?.insert(antiSpamBot, at: 0)
}
} else {
sortedAdmins = nil
}
let previous = previousPeers
previousPeers = sortedAdmins
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: peer, antiSpamBotId: antiSpamBotConfiguration.antiSpamBotId, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,9 @@
import Foundation
final class ChatRecentActionsInteraction {
let displayInfoAlert: () -> Void
init(displayInfoAlert: @escaping () -> Void) {
self.displayInfoAlert = displayInfoAlert
}
}
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import SearchBarNode
private let searchBarFont = Font.regular(17.0)
final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let cancel: () -> Void
private let searchBar: SearchBarNode
private var queryUpdated: ((String) -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void) {
self.theme = theme
self.strings = strings
self.cancel = cancel
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
let placeholderText = strings.Common_Search
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.cancel()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
override var nominalHeight: CGFloat {
return 54.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
@@ -0,0 +1,105 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private func generateArrowImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 5.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.beginPath()
context.move(to: CGPoint())
context.addLine(to: CGPoint(x: size.width, y: 0.0))
context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height))
context.closePath()
context.fillPath()
})
}
final class ChatRecentActionsTitleView: UIView {
private let button: HighlightTrackingButtonNode
private let titleNode: TextNode
private let arrowNode: ASImageNode
var color: UIColor {
didSet {
if self.color != oldValue {
self.setNeedsLayout()
}
}
}
var pressed: (() -> Void)?
var title: String = "" {
didSet {
if self.title != oldValue {
self.setNeedsLayout()
}
}
}
init(color: UIColor) {
self.color = color
self.button = HighlightTrackingButtonNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.image = generateArrowImage(color: color)
super.init(frame: CGRect())
self.addSubnode(self.titleNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.button)
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside])
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
strongSelf.arrowNode.alpha = 0.4
} else {
strongSelf.titleNode.alpha = 1.0
strongSelf.arrowNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
self.button.frame = CGRect(origin: CGPoint(), size: size)
let makeLayout = TextNode.asyncLayout(self.titleNode)
let (titleLayout, titleApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: self.color), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: floor((size.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
self.titleNode.frame = titleFrame
let _ = titleApply()
if let image = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 9.0), size: image.size)
}
}
@objc func buttonPressed() {
self.pressed?()
}
}