Support updated API

This commit is contained in:
Ali 2023-09-05 13:41:05 +04:00
parent f8ebd4aa2f
commit f4545aaeeb
38 changed files with 1402 additions and 956 deletions

View File

@ -0,0 +1,454 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import AccountContext
import TelegramPresentationData
import SwiftSignalKit
import AnimationCache
import MultiAnimationRenderer
import TelegramCore
import Postbox
import ChatListHeaderComponent
import ActionPanelComponent
import ChatFolderLinkPreviewScreen
final class ChatListContainerItemNode: ASDisplayNode {
private final class TopPanelItem {
let view = ComponentView<Empty>()
var size: CGSize?
init() {
}
}
private let context: AccountContext
private weak var controller: ChatListControllerImpl?
private let location: ChatListControllerLocation
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var presentationData: PresentationData
private let becameEmpty: (ChatListFilter?) -> Void
private let emptyAction: (ChatListFilter?) -> Void
private let secondaryEmptyAction: () -> Void
private let openArchiveSettings: () -> Void
private let isInlineMode: Bool
private var floatingHeaderOffset: CGFloat?
private(set) var emptyNode: ChatListEmptyNode?
var emptyShimmerEffectNode: ChatListShimmerNode?
private var shimmerNodeOffset: CGFloat = 0.0
let listNode: ChatListNode
private var topPanel: TopPanelItem?
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
private var peerDataDisposable: Disposable?
private var chatFolderUpdates: ChatFolderUpdates?
private var canReportPeer: Bool = false
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) {
self.context = context
self.controller = controller
self.location = location
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.presentationData = presentationData
self.becameEmpty = becameEmpty
self.emptyAction = emptyAction
self.secondaryEmptyAction = secondaryEmptyAction
self.openArchiveSettings = openArchiveSettings
self.isInlineMode = isInlineMode
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady)
if let controller, case .chatList(groupId: .root) = controller.location {
self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight
}
super.init()
self.addSubnode(self.listNode)
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
guard let strongSelf = self else {
return
}
var needsShimmerNode = false
var shimmerNodeOffset: CGFloat = 0.0
var needsEmptyNode = false
var hasOnlyArchive = false
var hasOnlyGeneralThread = false
var isLoading = false
switch isEmptyState {
case let .empty(isLoadingValue, hasArchiveInfo):
if hasArchiveInfo {
shimmerNodeOffset = 253.0
}
if isLoadingValue {
needsShimmerNode = true
needsEmptyNode = false
isLoading = isLoadingValue
} else {
needsEmptyNode = true
}
if !isLoadingValue {
strongSelf.becameEmpty(filter)
}
case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue):
needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue
hasOnlyArchive = onlyHasArchiveValue
hasOnlyGeneralThread = onlyGeneralThreadValue
}
if needsEmptyNode {
if let currentNode = strongSelf.emptyNode {
currentNode.updateIsLoading(isLoading)
} else {
let subject: ChatListEmptyNode.Subject
if let filter = filter {
var showEdit = true
if case let .filter(_, _, _, data) = filter {
if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty {
showEdit = false
}
}
subject = .filter(showEdit: showEdit)
} else {
if case .forum = location {
subject = .forum(hasGeneral: hasOnlyGeneralThread)
} else {
if case .chatList(groupId: .archive) = location {
subject = .archive
} else {
subject = .chats(hasArchive: hasOnlyArchive)
}
}
}
let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: {
self?.emptyAction(filter)
}, secondaryAction: {
self?.secondaryEmptyAction()
}, openArchiveSettings: {
self?.openArchiveSettings()
})
strongSelf.emptyNode = emptyNode
strongSelf.listNode.addSubnode(emptyNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
emptyNode.frame = emptyNodeFrame
emptyNode.updateLayout(size: size, insets: insets, transition: .immediate)
if let scrollingOffset = strongSelf.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
}
emptyNode.alpha = 0.0
transition.updateAlpha(node: emptyNode, alpha: 1.0)
}
} else if let emptyNode = strongSelf.emptyNode {
strongSelf.emptyNode = nil
transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in
emptyNode?.removeFromSupernode()
})
}
if needsShimmerNode {
strongSelf.shimmerNodeOffset = shimmerNodeOffset
if strongSelf.emptyShimmerEffectNode == nil {
let emptyShimmerEffectNode = ChatListShimmerNode()
strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode
strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate)
}
}
} else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.emptyShimmerEffectNode = nil
let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut)
emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in
emptyShimmerEffectNode?.removeFromSupernode()
})
strongSelf.listNode.alpha = 0.0
emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0)
}
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
guard let strongSelf = self else {
return
}
strongSelf.floatingHeaderOffset = offset
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
}
strongSelf.layoutAdditionalPanels(transition: transition)
}
if let filter, case let .filter(id, _, _, data) = filter, data.isShared {
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start()
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
var update = false
if let result, result.availableChatsToJoin != 0 {
if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin {
update = true
}
self.chatFolderUpdates = result
} else {
if self.chatFolderUpdates != nil {
self.chatFolderUpdates = nil
update = true
}
}
if update {
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
if case let .forum(peerId) = location {
self.peerDataDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] statusSettings in
guard let self else {
return
}
var canReportPeer = false
if let statusSettings, statusSettings.flags.contains(.canReport) {
canReportPeer = true
}
if self.canReportPeer != canReportPeer {
self.canReportPeer = canReportPeer
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
}
deinit {
self.pollFilterUpdatesDisposable?.dispose()
self.chatFilterUpdatesDisposable?.dispose()
self.peerDataDisposable?.dispose()
}
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate)
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
}
private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) {
guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else {
return
}
let _ = size
let _ = insets
if let topPanel = self.topPanel, let topPanelSize = topPanel.size {
let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height
if let topPanelView = topPanel.view.view {
var animateIn = false
var topPanelTransition = transition
if topPanelView.bounds.isEmpty {
topPanelTransition = .immediate
animateIn = true
}
topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize))
if animateIn {
transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height))
}
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
var listInsets = insets
var additionalTopInset: CGFloat = 0.0
if let chatFolderUpdates = self.chatFolderUpdates {
let topPanel: TopPanelItem
var topPanelTransition = Transition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin))
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .accent,
action: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents))
},
dismissAction: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else if self.canReportPeer {
let topPanel: TopPanelItem
var topPanelTransition = Transition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .destructive,
action: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let actionSheet = ActionSheetController(presentationData: self.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation),
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let self {
self.controller?.setInlineChatList(location: nil)
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).start()
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.listNode.present?(actionSheet)
},
dismissAction: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else {
if let topPanel = self.topPanel {
self.topPanel = nil
if let topPanelView = topPanel.view.view {
transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition)
if let scrollingOffset = self.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition)
}
}
self.layoutAdditionalPanels(transition: transition)
}
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.scrollingOffset = (navigationHeight, offset)
if let emptyNode = self.emptyNode {
emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
}
}
}

View File

@ -44,724 +44,6 @@ public enum ChatListContainerNodeFilter: Equatable {
} }
} }
private final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
private final class ChatListShimmerNode: ASDisplayNode {
private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectNode
private let maskNode: ASImageNode
private var currentParams: (size: CGSize, presentationData: PresentationData)?
override init() {
self.backgroundColorNode = ASDisplayNode()
self.effectNode = ShimmerEffectNode()
self.maskNode = ASImageNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.maskNode)
}
func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
self.currentParams = (size, presentationData)
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil))
let timestamp1: Int32 = 100000
let peers: [EnginePeer.Id: EnginePeer] = [:]
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in
let message = EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp1,
flags: [],
tags: [],
globalTags: [],
localTags: [],
forwardInfo: nil,
author: peer1,
text: "Text",
attributes: [],
media: [],
peers: peers,
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
let readState = EnginePeerReadCounters()
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
messages: [message],
peer: EngineRenderedPeer(peer: peer1),
threadInfo: nil,
combinedReadState: readState,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
}
var itemNodes: [ChatListItemNode] = []
for i in 0 ..< items.count {
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
if let itemNode = node as? ChatListItemNode {
itemNodes.append(itemNode)
}
apply().1(ListViewItemApply(isOnScreen: true))
})
}
self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let fakeLabelPlaceholderHeight: CGFloat = 8.0
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
let startPoint = origin
let diameter = fakeLabelPlaceholderHeight
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
}
while currentY < size.height {
let sampleIndex = 0
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
if !isInlineMode {
if !itemNodes[sampleIndex].avatarNode.isHidden {
context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY))
}
}
let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0)
} else {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0)
}
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0)))
}
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0)
context.setBlendMode(.normal)
context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor)
context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY))
currentY += itemHeight
}
})
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
}
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
}
}
private final class ChatListContainerItemNode: ASDisplayNode {
private final class TopPanelItem {
let view = ComponentView<Empty>()
var size: CGSize?
init() {
}
}
private let context: AccountContext
private weak var controller: ChatListControllerImpl?
private let location: ChatListControllerLocation
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var presentationData: PresentationData
private let becameEmpty: (ChatListFilter?) -> Void
private let emptyAction: (ChatListFilter?) -> Void
private let secondaryEmptyAction: () -> Void
private let openArchiveSettings: () -> Void
private let isInlineMode: Bool
private var floatingHeaderOffset: CGFloat?
private(set) var emptyNode: ChatListEmptyNode?
var emptyShimmerEffectNode: ChatListShimmerNode?
private var shimmerNodeOffset: CGFloat = 0.0
let listNode: ChatListNode
private var topPanel: TopPanelItem?
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
private var peerDataDisposable: Disposable?
private var chatFolderUpdates: ChatFolderUpdates?
private var canReportPeer: Bool = false
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) {
self.context = context
self.controller = controller
self.location = location
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.presentationData = presentationData
self.becameEmpty = becameEmpty
self.emptyAction = emptyAction
self.secondaryEmptyAction = secondaryEmptyAction
self.openArchiveSettings = openArchiveSettings
self.isInlineMode = isInlineMode
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady)
if let controller, case .chatList(groupId: .root) = controller.location {
self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight
}
super.init()
self.addSubnode(self.listNode)
self.listNode.isEmptyUpdated = { [weak self] isEmptyState, _, transition in
guard let strongSelf = self else {
return
}
var needsShimmerNode = false
var shimmerNodeOffset: CGFloat = 0.0
var needsEmptyNode = false
var hasOnlyArchive = false
var hasOnlyGeneralThread = false
var isLoading = false
switch isEmptyState {
case let .empty(isLoadingValue, hasArchiveInfo):
if hasArchiveInfo {
shimmerNodeOffset = 253.0
}
if isLoadingValue {
needsShimmerNode = true
needsEmptyNode = false
isLoading = isLoadingValue
} else {
needsEmptyNode = true
}
if !isLoadingValue {
strongSelf.becameEmpty(filter)
}
case let .notEmpty(_, onlyHasArchiveValue, onlyGeneralThreadValue):
needsEmptyNode = onlyHasArchiveValue || onlyGeneralThreadValue
hasOnlyArchive = onlyHasArchiveValue
hasOnlyGeneralThread = onlyGeneralThreadValue
}
if needsEmptyNode {
if let currentNode = strongSelf.emptyNode {
currentNode.updateIsLoading(isLoading)
} else {
let subject: ChatListEmptyNode.Subject
if let filter = filter {
var showEdit = true
if case let .filter(_, _, _, data) = filter {
if data.excludeRead && data.includePeers.peers.isEmpty && data.includePeers.pinnedPeers.isEmpty {
showEdit = false
}
}
subject = .filter(showEdit: showEdit)
} else {
if case .forum = location {
subject = .forum(hasGeneral: hasOnlyGeneralThread)
} else {
if case .chatList(groupId: .archive) = location {
subject = .archive
} else {
subject = .chats(hasArchive: hasOnlyArchive)
}
}
}
let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: {
self?.emptyAction(filter)
}, secondaryAction: {
self?.secondaryEmptyAction()
}, openArchiveSettings: {
self?.openArchiveSettings()
})
strongSelf.emptyNode = emptyNode
strongSelf.listNode.addSubnode(emptyNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
emptyNode.frame = emptyNodeFrame
emptyNode.updateLayout(size: size, insets: insets, transition: .immediate)
if let scrollingOffset = strongSelf.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
}
}
emptyNode.alpha = 0.0
transition.updateAlpha(node: emptyNode, alpha: 1.0)
}
} else if let emptyNode = strongSelf.emptyNode {
strongSelf.emptyNode = nil
transition.updateAlpha(node: emptyNode, alpha: 0.0, completion: { [weak emptyNode] _ in
emptyNode?.removeFromSupernode()
})
}
if needsShimmerNode {
strongSelf.shimmerNodeOffset = shimmerNodeOffset
if strongSelf.emptyShimmerEffectNode == nil {
let emptyShimmerEffectNode = ChatListShimmerNode()
strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode
strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode)
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate)
}
}
} else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.emptyShimmerEffectNode = nil
let emptyNodeTransition = transition.isAnimated ? transition : .animated(duration: 0.3, curve: .easeInOut)
emptyNodeTransition.updateAlpha(node: emptyShimmerEffectNode, alpha: 0.0, completion: { [weak emptyShimmerEffectNode] _ in
emptyShimmerEffectNode?.removeFromSupernode()
})
strongSelf.listNode.alpha = 0.0
emptyNodeTransition.updateAlpha(node: strongSelf.listNode, alpha: 1.0)
}
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
guard let strongSelf = self else {
return
}
strongSelf.floatingHeaderOffset = offset
if let (size, insets, _, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
}
strongSelf.layoutAdditionalPanels(transition: transition)
}
if let filter, case let .filter(id, _, _, data) = filter, data.isShared {
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start()
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
var update = false
if let result, result.availableChatsToJoin != 0 {
if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin {
update = true
}
self.chatFolderUpdates = result
} else {
if self.chatFolderUpdates != nil {
self.chatFolderUpdates = nil
update = true
}
}
if update {
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
if case let .forum(peerId) = location {
self.peerDataDisposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] statusSettings in
guard let self else {
return
}
var canReportPeer = false
if let statusSettings, statusSettings.flags.contains(.canReport) {
canReportPeer = true
}
if self.canReportPeer != canReportPeer {
self.canReportPeer = canReportPeer
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
}
deinit {
self.pollFilterUpdatesDisposable?.dispose()
self.chatFilterUpdatesDisposable?.dispose()
self.peerDataDisposable?.dispose()
}
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
node.update(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, size: size, isInlineMode: self.isInlineMode, presentationData: self.presentationData, transition: .immediate)
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
}
private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) {
guard let (size, insets, visualNavigationHeight, _, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else {
return
}
let _ = size
let _ = insets
if let topPanel = self.topPanel, let topPanelSize = topPanel.size {
let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height
if let topPanelView = topPanel.view.view {
var animateIn = false
var topPanelTransition = transition
if topPanelView.bounds.isEmpty {
topPanelTransition = .immediate
animateIn = true
}
topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize))
if animateIn {
transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height))
}
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.listNode.updateThemeAndStrings(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
self.emptyNode?.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
var listInsets = insets
var additionalTopInset: CGFloat = 0.0
if let chatFolderUpdates = self.chatFolderUpdates {
let topPanel: TopPanelItem
var topPanelTransition = Transition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin))
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .accent,
action: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents))
},
dismissAction: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else if self.canReportPeer {
let topPanel: TopPanelItem
var topPanelTransition = Transition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
let title: String = self.presentationData.strings.Conversation_ReportSpamAndLeave
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
color: .destructive,
action: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let actionSheet = ActionSheetController(presentationData: self.presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: self.presentationData.strings.Conversation_ReportSpamGroupConfirmation),
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let self {
self.controller?.setInlineChatList(location: nil)
let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true).start()
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.listNode.present?(actionSheet)
},
dismissAction: { [weak self] in
guard let self, case let .forum(peerId) = self.location else {
return
}
let _ = self.context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else {
if let topPanel = self.topPanel {
self.topPanel = nil
if let topPanelView = topPanel.view.view {
transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, storiesInset: storiesInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
emptyNode.updateLayout(size: emptyNodeFrame.size, insets: listInsets, transition: transition)
if let scrollingOffset = self.scrollingOffset {
emptyNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: transition)
}
}
self.layoutAdditionalPanels(transition: transition)
}
func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.scrollingOffset = (navigationHeight, offset)
if let emptyNode = self.emptyNode {
emptyNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
}
}
}
public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private let context: AccountContext private let context: AccountContext
private weak var controller: ChatListControllerImpl? private weak var controller: ChatListControllerImpl?

View File

@ -2345,19 +2345,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
for item in items { for item in items {
switch item { switch item {
case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _, _): case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _, _):
if case .user = peer { storyStatsIds.append(peer.id)
storyStatsIds.append(peer.id)
}
case let .localPeer(peer, _, _, _, _, _, _, _, _, _): case let .localPeer(peer, _, _, _, _, _, _, _, _, _):
if case .user = peer { storyStatsIds.append(peer.id)
storyStatsIds.append(peer.id)
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _): case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _):
if foundPeer.peer is TelegramUser { storyStatsIds.append(foundPeer.peer.id)
storyStatsIds.append(foundPeer.peer.id)
}
case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _): case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _):
if let peer = peer.peer, case .user = peer { if let peer = peer.peer {
storyStatsIds.append(peer.id) storyStatsIds.append(peer.id)
} }
default: default:
@ -3386,7 +3380,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} }
} }
private final class ShimmerEffectNode: ASDisplayNode { private final class SearchShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor? private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor? private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode private let imageNodeContainer: ASDisplayNode
@ -3504,13 +3498,13 @@ private final class ShimmerEffectNode: ASDisplayNode {
public final class ChatListSearchShimmerNode: ASDisplayNode { public final class ChatListSearchShimmerNode: ASDisplayNode {
private let backgroundColorNode: ASDisplayNode private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectNode private let effectNode: SearchShimmerEffectNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)? private var currentParams: (size: CGSize, presentationData: PresentationData, key: ChatListSearchPaneKey)?
public init(key: ChatListSearchPaneKey) { public init(key: ChatListSearchPaneKey) {
self.backgroundColorNode = ASDisplayNode() self.backgroundColorNode = ASDisplayNode()
self.effectNode = ShimmerEffectNode() self.effectNode = SearchShimmerEffectNode()
self.maskNode = ASImageNode() self.maskNode = ASImageNode()
super.init() super.init()

View File

@ -0,0 +1,288 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
import AnimationCache
import MultiAnimationRenderer
import TelegramCore
final class ShimmerEffectNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
self.updateAnimation()
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
final class ChatListShimmerNode: ASDisplayNode {
private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectNode
private let maskNode: ASImageNode
private var currentParams: (size: CGSize, presentationData: PresentationData)?
override init() {
self.backgroundColorNode = ASDisplayNode()
self.effectNode = ShimmerEffectNode()
self.maskNode = ASImageNode()
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.maskNode)
}
func update(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
self.currentParams = (size, presentationData)
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil))
let timestamp1: Int32 = 100000
let peers: [EnginePeer.Id: EnginePeer] = [:]
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
interaction.isInlineMode = isInlineMode
let items = (0 ..< 2).map { _ -> ChatListItem in
let message = EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp1,
flags: [],
tags: [],
globalTags: [],
localTags: [],
forwardInfo: nil,
author: peer1,
text: "Text",
attributes: [],
media: [],
peers: peers,
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
let readState = EnginePeerReadCounters()
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
messages: [message],
peer: EngineRenderedPeer(peer: peer1),
threadInfo: nil,
combinedReadState: readState,
isRemovedFromTotalUnreadCount: false,
presence: nil,
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
inputActivities: nil,
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
}
var itemNodes: [ChatListItemNode] = []
for i in 0 ..< items.count {
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
if let itemNode = node as? ChatListItemNode {
itemNodes.append(itemNode)
}
apply().1(ListViewItemApply(isOnScreen: true))
})
}
self.backgroundColorNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let fakeLabelPlaceholderHeight: CGFloat = 8.0
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
let startPoint = origin
let diameter = fakeLabelPlaceholderHeight
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
}
while currentY < size.height {
let sampleIndex = 0
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
if !isInlineMode {
if !itemNodes[sampleIndex].avatarNode.isHidden {
context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY))
}
}
let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX + 22.0, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0 - 22.0)
} else {
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 60.0)
}
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
if isInlineMode {
context.fillEllipse(in: CGRect(origin: CGPoint(x: textFrame.minX, y: titleFrame.minY + 2.0), size: CGSize(width: 16.0, height: 16.0)))
}
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY), width: 30.0)
context.setBlendMode(.normal)
context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor)
context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY))
currentY += itemHeight
}
})
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
}
transition.updateFrame(node: self.backgroundColorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
}
}

View File

@ -23,8 +23,6 @@ import ChatListHeaderComponent
import UndoUI import UndoUI
import NewSessionInfoScreen import NewSessionInfoScreen
private var debugDidAddNewSessionReview = false
public enum ChatListNodeMode { public enum ChatListNodeMode {
case chatList(appendContacts: Bool) case chatList(appendContacts: Bool)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, displayPresence: Bool)
@ -1637,11 +1635,11 @@ public final class ChatListNode: ListView {
return true return true
})) }))
let _ = removeNewSessionReviews(postbox: self.context.account.postbox, ids: [newSessionReview.id]).start() let _ = self.context.engine.privacy.confirmNewSessionReview(id: newSessionReview.id)
} else { } else {
self.push?(NewSessionInfoScreen(context: self.context, newSessionReview: newSessionReview)) self.push?(NewSessionInfoScreen(context: self.context, newSessionReview: newSessionReview))
let _ = removeNewSessionReviews(postbox: self.context.account.postbox, ids: [newSessionReview.id]).start() let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).start()
} }
}, openChatFolderUpdates: { [weak self] in }, openChatFolderUpdates: { [weak self] in
guard let self else { guard let self else {
@ -1743,12 +1741,7 @@ public final class ChatListNode: ListView {
let suggestedChatListNotice: Signal<ChatListNotice?, NoError> let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
if case .chatList(groupId: .root) = location, chatListFilter == nil { if case .chatList(groupId: .root) = location, chatListFilter == nil {
#if DEBUG let _ = context.engine.privacy.cleanupSessionReviews().start()
if !debugDidAddNewSessionReview {
debugDidAddNewSessionReview = true
let _ = addNewSessionReview(postbox: context.account.postbox, item: NewSessionReview(id: 1, device: "iPhone 14 Pro", location: "Dubai, UAE")).start()
}
#endif
suggestedChatListNotice = .single(nil) suggestedChatListNotice = .single(nil)
|> then ( |> then (
@ -2952,6 +2945,15 @@ public final class ChatListNode: ListView {
} }
} }
self.dynamicVisualInsets = { [weak self] in
guard let self else {
return UIEdgeInsets()
}
let _ = self
return UIEdgeInsets()
}
self.pollFilterUpdates() self.pollFilterUpdates()
self.resetFilter() self.resetFilter()

View File

@ -112,12 +112,12 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
self.contentContainer.clipsToBounds = true self.contentContainer.clipsToBounds = true
self.clipsToBounds = true self.clipsToBounds = true
self.addSubnode(self.separatorNode)
self.contentContainer.addSubnode(self.titleNode) self.contentContainer.addSubnode(self.titleNode)
self.contentContainer.addSubnode(self.textNode) self.contentContainer.addSubnode(self.textNode)
self.contentContainer.addSubnode(self.arrowNode) self.contentContainer.addSubnode(self.arrowNode)
self.addSubnode(self.contentContainer) self.addSubnode(self.contentContainer)
self.addSubnode(self.separatorNode)
self.zPosition = 1.0 self.zPosition = 1.0
} }

View File

@ -189,6 +189,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
public private(set) final var visibleSize: CGSize = CGSize() public private(set) final var visibleSize: CGSize = CGSize()
public private(set) final var insets = UIEdgeInsets() public private(set) final var insets = UIEdgeInsets()
public final var visualInsets: UIEdgeInsets? public final var visualInsets: UIEdgeInsets?
public final var dynamicVisualInsets: (() -> UIEdgeInsets)?
public private(set) final var headerInsets = UIEdgeInsets() public private(set) final var headerInsets = UIEdgeInsets()
public private(set) final var scrollIndicatorInsets = UIEdgeInsets() public private(set) final var scrollIndicatorInsets = UIEdgeInsets()
private final var ensureTopInsetForOverlayHighlightedItems: CGFloat? private final var ensureTopInsetForOverlayHighlightedItems: CGFloat?
@ -4388,7 +4389,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if abs(apparentHeightDelta) > CGFloat.ulpOfOne { if abs(apparentHeightDelta) > CGFloat.ulpOfOne {
itemNode.updateFrame(itemNode.frame, within: self.visibleSize) itemNode.updateFrame(itemNode.frame, within: self.visibleSize)
let visualInsets = self.visualInsets ?? self.insets let visualInsets = self.dynamicVisualInsets?() ?? self.visualInsets ?? self.insets
if itemNode.apparentFrame.maxY <= visualInsets.top { if itemNode.apparentFrame.maxY <= visualInsets.top {
offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta) offsetRanges.offset(IndexRange(first: 0, last: index), offset: -apparentHeightDelta)

View File

@ -20,7 +20,7 @@ private let rankMaxLength: Int32 = 16
private final class ChannelAdminControllerArguments { private final class ChannelAdminControllerArguments {
let context: AccountContext let context: AccountContext
let updateAdminRights: (Bool) -> Void let updateAdminRights: (Bool) -> Void
let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let toggleRight: (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void
let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void
let transferOwnership: () -> Void let transferOwnership: () -> Void
let updateRank: (String, String) -> Void let updateRank: (String, String) -> Void
@ -28,8 +28,9 @@ private final class ChannelAdminControllerArguments {
let dismissAdmin: () -> Void let dismissAdmin: () -> Void
let dismissInput: () -> Void let dismissInput: () -> Void
let animateError: () -> Void let animateError: () -> Void
let toggleIsOptionExpanded: (RightsItem.Sub) -> Void
init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void) {
self.context = context self.context = context
self.updateAdminRights = updateAdminRights self.updateAdminRights = updateAdminRights
self.toggleRight = toggleRight self.toggleRight = toggleRight
@ -40,6 +41,7 @@ private final class ChannelAdminControllerArguments {
self.dismissAdmin = dismissAdmin self.dismissAdmin = dismissAdmin
self.dismissInput = dismissInput self.dismissInput = dismissInput
self.animateError = animateError self.animateError = animateError
self.toggleIsOptionExpanded = toggleIsOptionExpanded
} }
} }
@ -71,12 +73,41 @@ private enum ChannelAdminEntryStableId: Hashable {
case rankInfo case rankInfo
case adminRights case adminRights
case rightsTitle case rightsTitle
case right(TelegramChatAdminRightsFlags) case right(RightsItem)
case addAdminsInfo case addAdminsInfo
case transfer case transfer
case dismiss case dismiss
} }
private struct AdminSubPermission: Equatable {
var title: String
var flags: TelegramChatAdminRightsFlags
var isSelected: Bool
var isEnabled: Bool
}
enum RightsItem: Equatable, Hashable {
enum Sub {
case messages
case stories
}
case direct(TelegramChatAdminRightsFlags)
case sub(Sub, [TelegramChatAdminRightsFlags])
}
private let messageRelatedFlags: [TelegramChatAdminRightsFlags] = [
.canPostMessages,
.canEditMessages,
.canDeleteMessages
]
private let storiesRelatedFlags: [TelegramChatAdminRightsFlags] = [
.canPostStories,
.canEditStories,
.canDeleteStories
]
private enum ChannelAdminEntry: ItemListNodeEntry { private enum ChannelAdminEntry: ItemListNodeEntry {
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?) case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, EnginePeer.Presence?)
case rankTitle(PresentationTheme, String, Int32?, Int32) case rankTitle(PresentationTheme, String, Int32?, Int32)
@ -84,7 +115,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
case rankInfo(PresentationTheme, String, Bool) case rankInfo(PresentationTheme, String, Bool)
case adminRights(PresentationTheme, String, Bool) case adminRights(PresentationTheme, String, Bool)
case rightsTitle(PresentationTheme, String) case rightsTitle(PresentationTheme, String)
case rightItem(PresentationTheme, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool) case rightItem(PresentationTheme, Int, String, RightsItem, TelegramChatAdminRightsFlags, Bool, Bool, [AdminSubPermission], Bool)
case addAdminsInfo(PresentationTheme, String) case addAdminsInfo(PresentationTheme, String)
case transfer(PresentationTheme, String) case transfer(PresentationTheme, String)
case dismiss(PresentationTheme, String) case dismiss(PresentationTheme, String)
@ -120,7 +151,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
return .adminRights return .adminRights
case .rightsTitle: case .rightsTitle:
return .rightsTitle return .rightsTitle
case let .rightItem(_, _, _, right, _, _, _): case let .rightItem(_, _, _, right, _, _, _, _, _):
return .right(right) return .right(right)
case .addAdminsInfo: case .addAdminsInfo:
return .addAdminsInfo return .addAdminsInfo
@ -185,8 +216,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled, lhsSubItems, lhsIsExpanded):
if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled, rhsSubItems, rhsIsExpanded) = rhs {
if lhsTheme !== rhsTheme { if lhsTheme !== rhsTheme {
return false return false
} }
@ -208,6 +239,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
if lhsEnabled != rhsEnabled { if lhsEnabled != rhsEnabled {
return false return false
} }
if lhsSubItems != rhsSubItems {
return false
}
if lhsIsExpanded != rhsIsExpanded {
return false
}
return true return true
} else { } else {
return false return false
@ -256,11 +293,11 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
default: default:
return true return true
} }
case let .rightItem(_, lhsIndex, _, _, _, _, _): case let .rightItem(_, lhsIndex, _, _, _, _, _, _, _):
switch rhs { switch rhs {
case .info, .adminRights, .rightsTitle: case .info, .adminRights, .rightsTitle:
return false return false
case let .rightItem(_, rhsIndex, _, _, _, _, _): case let .rightItem(_, rhsIndex, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
default: default:
return true return true
@ -341,12 +378,48 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
}) })
case let .rightsTitle(_, text): case let .rightsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .rightItem(_, _, text, right, flags, value, enabled): case let .rightItem(_, _, text, right, flags, value, enabled, subPermissions, isExpanded):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in if !subPermissions.isEmpty {
arguments.toggleRight(right, flags) return ItemListExpandableSwitchItem(presentationData: presentationData, title: text, value: value, isExpanded: isExpanded, subItems: subPermissions.map { item in
}, activatedWhileDisabled: { return ItemListExpandableSwitchItem.SubItem(
arguments.toggleRightWhileDisabled(right, flags) id: AnyHashable(item.flags.rawValue),
}) title: item.title,
isSelected: item.isSelected,
isEnabled: item.isEnabled
)
}, type: .icon, enableInteractiveChanges: enabled, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
if enabled {
arguments.toggleRight(right, flags, value)
} else {
//arguments.toggleRightWhileDisabled(right, flags)
}
}, activatedWhileDisabled: {
//arguments.toggleRightWhileDisabled(right, flags)
}, selectAction: {
if case let .sub(type, _) = right {
arguments.toggleIsOptionExpanded(type)
}
}, subAction: { item in
guard let value = item.id.base as? Int32 else {
return
}
let subRights = TelegramChatAdminRightsFlags(rawValue: value)
if enabled {
arguments.toggleRight(.direct(subRights), flags, !item.isSelected)
} else {
arguments.toggleRightWhileDisabled(subRights, flags)
}
})
} else {
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleRight(right, flags, value)
}, activatedWhileDisabled: {
if case let .direct(right) = right {
arguments.toggleRightWhileDisabled(right, flags)
}
})
}
case let .addAdminsInfo(_, text): case let .addAdminsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .transfer(_, text): case let .transfer(_, text):
@ -362,18 +435,20 @@ private enum ChannelAdminEntry: ItemListNodeEntry {
} }
private struct ChannelAdminControllerState: Equatable { private struct ChannelAdminControllerState: Equatable {
let adminRights: Bool var adminRights: Bool
let updatedFlags: TelegramChatAdminRightsFlags? var updatedFlags: TelegramChatAdminRightsFlags?
let updatedRank: String? var updatedRank: String?
let updating: Bool var updating: Bool
let focusedOnRank: Bool var focusedOnRank: Bool
var expandedPermissions: Set<RightsItem.Sub> = Set()
init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false) { init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false, expandedPermissions: Set<RightsItem.Sub> = Set()) {
self.adminRights = adminRights self.adminRights = adminRights
self.updatedFlags = updatedFlags self.updatedFlags = updatedFlags
self.updatedRank = updatedRank self.updatedRank = updatedRank
self.updating = updating self.updating = updating
self.focusedOnRank = focusedOnRank self.focusedOnRank = focusedOnRank
self.expandedPermissions = expandedPermissions
} }
static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool { static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool {
@ -392,27 +467,30 @@ private struct ChannelAdminControllerState: Equatable {
if lhs.focusedOnRank != rhs.focusedOnRank { if lhs.focusedOnRank != rhs.focusedOnRank {
return false return false
} }
if lhs.expandedPermissions != rhs.expandedPermissions {
return false
}
return true return true
} }
func withUpdatedAdminRights(_ adminRights: Bool) -> ChannelAdminControllerState { func withUpdatedAdminRights(_ adminRights: Bool) -> ChannelAdminControllerState {
return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions)
} }
func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState { func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState {
return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions)
} }
func withUpdatedUpdatedRank(_ updatedRank: String?) -> ChannelAdminControllerState { func withUpdatedUpdatedRank(_ updatedRank: String?) -> ChannelAdminControllerState {
return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions)
} }
func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState {
return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank) return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions)
} }
func withUpdatedFocusedOnRank(_ focusedOnRank: Bool) -> ChannelAdminControllerState { func withUpdatedFocusedOnRank(_ focusedOnRank: Bool) -> ChannelAdminControllerState {
return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank) return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank, expandedPermissions: self.expandedPermissions)
} }
} }
@ -422,7 +500,12 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm
} else if right.contains(.canPostMessages) { } else if right.contains(.canPostMessages) {
return strings.Channel_EditAdmin_PermissionPostMessages return strings.Channel_EditAdmin_PermissionPostMessages
} else if right.contains(.canEditMessages) { } else if right.contains(.canEditMessages) {
return strings.Channel_EditAdmin_PermissionEditMessages if isChannel {
//TODO:localize
return "Edit Messages of Others"
} else {
return strings.Channel_EditAdmin_PermissionEditMessages
}
} else if right.contains(.canDeleteMessages) { } else if right.contains(.canDeleteMessages) {
return isGroup ? strings.Channel_EditAdmin_PermissionDeleteMessages : strings.Channel_EditAdmin_PermissionDeleteMessagesOfOthers return isGroup ? strings.Channel_EditAdmin_PermissionDeleteMessages : strings.Channel_EditAdmin_PermissionDeleteMessagesOfOthers
} else if right.contains(.canBanUsers) { } else if right.contains(.canBanUsers) {
@ -451,6 +534,15 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm
} else { } else {
return strings.Channel_AdminLog_CanManageCalls return strings.Channel_AdminLog_CanManageCalls
} }
} else if right.contains(.canPostStories) {
//TODO:localize
return "Post Stories"
} else if right.contains(.canEditStories) {
//TODO:localize
return "Edit Stories of Others"
} else if right.contains(.canDeleteStories) {
//TODO:localize
return "Delete Stories of Others"
} else { } else {
return "" return ""
} }
@ -553,45 +645,45 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
let isGroup: Bool let isGroup: Bool
var maskRightsFlags: TelegramChatAdminRightsFlags var maskRightsFlags: TelegramChatAdminRightsFlags
let rightsOrder: [TelegramChatAdminRightsFlags]
let rightsOrder: [RightsItem]
maskRightsFlags = TelegramChatAdminRightsFlags.peerSpecific(peer: .channel(channel)) maskRightsFlags = TelegramChatAdminRightsFlags.peerSpecific(peer: .channel(channel))
switch channel.info { switch channel.info {
case .broadcast: case .broadcast:
isGroup = false isGroup = false
rightsOrder = [ rightsOrder = [
.canChangeInfo, .direct(.canChangeInfo),
.canPostMessages, .sub(.messages, messageRelatedFlags),
.canEditMessages, .sub(.stories, storiesRelatedFlags),
.canDeleteMessages, .direct(.canInviteUsers),
.canInviteUsers, .direct(.canManageCalls),
.canManageCalls, .direct(.canAddAdmins)
.canAddAdmins
] ]
case .group: case .group:
isGroup = true isGroup = true
if channel.flags.contains(.isForum) { if channel.flags.contains(.isForum) {
rightsOrder = [ rightsOrder = [
.canChangeInfo, .direct(.canChangeInfo),
.canDeleteMessages, .direct(.canDeleteMessages),
.canBanUsers, .direct(.canBanUsers),
.canInviteUsers, .direct(.canInviteUsers),
.canPinMessages, .direct(.canPinMessages),
.canManageTopics, .direct(.canManageTopics),
.canManageCalls, .direct(.canManageCalls),
.canBeAnonymous, .direct(.canBeAnonymous),
.canAddAdmins .direct(.canAddAdmins)
] ]
} else { } else {
rightsOrder = [ rightsOrder = [
.canChangeInfo, .direct(.canChangeInfo),
.canDeleteMessages, .direct(.canDeleteMessages),
.canBanUsers, .direct(.canBanUsers),
.canInviteUsers, .direct(.canInviteUsers),
.canPinMessages, .direct(.canPinMessages),
.canManageCalls, .direct(.canManageCalls),
.canBeAnonymous, .direct(.canBeAnonymous),
.canAddAdmins .direct(.canAddAdmins)
] ]
} }
} }
@ -621,11 +713,52 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
} }
var index = 0 var index = 0
for right in rightsOrder { rightsLoop: for right in rightsOrder {
if accountUserRightsFlags.contains(right) { let enabled: Bool
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), right == .canBeAnonymous)) let isSelected: Bool
index += 1 let itemTitle: String
var subItems: [AdminSubPermission] = []
var isExpanded = false
switch right {
case let .direct(right):
if !accountUserRightsFlags.contains(right) {
continue rightsLoop
}
enabled = right == .canBeAnonymous
itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights)
isSelected = currentRightsFlags.contains(right)
case let .sub(type, subRights):
let filteredSubRights = subRights.filter({ accountUserRightsFlags.contains($0) })
if filteredSubRights.isEmpty {
continue rightsLoop
}
enabled = true
//TODO:localize
switch type {
case .messages:
itemTitle = "Manage Messages"
case .stories:
itemTitle = "Manage Stories"
}
isSelected = subRights.allSatisfy({ currentRightsFlags.contains($0) })
isExpanded = state.expandedPermissions.contains(type)
for subRight in filteredSubRights {
let subRightEnabled = true
subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: currentRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled))
}
} }
entries.append(.rightItem(presentationData.theme, index, itemTitle, right, currentRightsFlags, isSelected, enabled, subItems, isExpanded))
index += 1
} }
} }
} else { } else {
@ -660,11 +793,52 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
} }
var index = 0 var index = 0
for right in rightsOrder { rightsLoop: for right in rightsOrder {
if accountUserRightsFlags.contains(right) { let enabled: Bool
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: right))) let isSelected: Bool
index += 1 let itemTitle: String
var subItems: [AdminSubPermission] = []
var isExpanded = false
switch right {
case let .direct(right):
if !accountUserRightsFlags.contains(right) {
continue rightsLoop
}
enabled = !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: right)
itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights)
isSelected = currentRightsFlags.contains(right)
case let .sub(type, subRights):
let filteredSubRights = subRights.filter({ accountUserRightsFlags.contains($0) })
if filteredSubRights.isEmpty {
continue rightsLoop
}
enabled = !state.updating
//TODO:localize
switch type {
case .messages:
itemTitle = "Manage Messages"
case .stories:
itemTitle = "Manage Stories"
}
isSelected = subRights.allSatisfy({ currentRightsFlags.contains($0) })
isExpanded = state.expandedPermissions.contains(type)
for subRight in filteredSubRights {
let subRightEnabled = !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: .channel(channel), right: subRight)
subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: currentRightsFlags.contains(subRight), isEnabled: enabled && subRightEnabled))
}
} }
entries.append(.rightItem(presentationData.theme, index, itemTitle, right, currentRightsFlags, isSelected, enabled, subItems, isExpanded))
index += 1
} }
if accountUserRightsFlags.contains(.canAddAdmins) { if accountUserRightsFlags.contains(.canAddAdmins) {
@ -693,8 +867,43 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
} }
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo {
var index = 0 var index = 0
for right in rightsOrder { rightsLoop: for right in rightsOrder {
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), right, adminInfo.rights.rights, adminInfo.rights.rights.contains(right), false)) let enabled: Bool = false
let isSelected: Bool
let itemTitle: String
var subItems: [AdminSubPermission] = []
var isExpanded = false
switch right {
case let .direct(right):
itemTitle = stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights)
isSelected = adminInfo.rights.rights.contains(right)
case let .sub(type, subRights):
let filteredSubRights = subRights
if filteredSubRights.isEmpty {
continue rightsLoop
}
//TODO:localize
switch type {
case .messages:
itemTitle = "Manage Messages"
case .stories:
itemTitle = "Manage Stories"
}
isSelected = subRights.allSatisfy({ adminInfo.rights.rights.contains($0) })
isExpanded = state.expandedPermissions.contains(type)
for subRight in filteredSubRights {
let subRightEnabled = false
subItems.append(AdminSubPermission(title: stringForRight(strings: presentationData.strings, right: subRight, isGroup: isGroup, isChannel: isChannel, isForum: channel.flags.contains(.isForum), defaultBannedRights: channel.defaultBannedRights), flags: subRight, isSelected: adminInfo.rights.rights.contains(subRight), isEnabled: enabled && subRightEnabled))
}
}
entries.append(.rightItem(presentationData.theme, index, itemTitle, right, adminInfo.rights.rights, isSelected, enabled, subItems, isExpanded))
index += 1 index += 1
} }
} }
@ -792,7 +1001,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
var index = 0 var index = 0
for right in rightsOrder { for right in rightsOrder {
if accountUserRightsFlags.contains(right) { if accountUserRightsFlags.contains(right) {
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: false, defaultBannedRights: group.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator)) entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, isForum: false, defaultBannedRights: group.defaultBannedRights), .direct(right), currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator, [], false))
index += 1 index += 1
} }
} }
@ -866,13 +1075,25 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
updateState { current in updateState { current in
return current.withUpdatedAdminRights(value) return current.withUpdatedAdminRights(value)
} }
}, toggleRight: { right, flags in }, toggleRight: { right, flags, value in
updateState { current in updateState { current in
var updated = flags var updated = flags
if flags.contains(right) {
updated.remove(right) var combinedRight: TelegramChatAdminRightsFlags
switch right {
case let .direct(right):
combinedRight = right
case let .sub(_, right):
combinedRight = []
for flag in right {
combinedRight.insert(flag)
}
}
if !value {
updated.remove(combinedRight)
} else { } else {
updated.insert(right) updated.insert(combinedRight)
} }
return current.withUpdatedUpdatedFlags(updated) return current.withUpdatedUpdatedFlags(updated)
} }
@ -971,6 +1192,18 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
dismissInputImpl?() dismissInputImpl?()
}, animateError: { }, animateError: {
errorImpl?() errorImpl?()
}, toggleIsOptionExpanded: { flag in
updateState { state in
var state = state
if state.expandedPermissions.contains(flag) {
state.expandedPermissions.remove(flag)
} else {
state.expandedPermissions.insert(flag)
}
return state
}
}) })
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData

View File

@ -160,7 +160,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) } dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) }
dict[-566281095] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsRecent($0) } dict[-566281095] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsRecent($0) }
dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) } dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) }
dict[-2094689180] = { return Api.Chat.parse_channel($0) } dict[-1795845413] = { return Api.Chat.parse_channel($0) }
dict[399807445] = { return Api.Chat.parse_channelForbidden($0) } dict[399807445] = { return Api.Chat.parse_channelForbidden($0) }
dict[1103884886] = { return Api.Chat.parse_chat($0) } dict[1103884886] = { return Api.Chat.parse_chat($0) }
dict[693512293] = { return Api.Chat.parse_chatEmpty($0) } dict[693512293] = { return Api.Chat.parse_chatEmpty($0) }
@ -911,7 +911,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) } dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) }
dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) } dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) }
dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) } dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) }
dict[1757493555] = { return Api.Update.parse_updateReadMessagesContents($0) } dict[-131960447] = { return Api.Update.parse_updateReadMessagesContents($0) }
dict[-145845461] = { return Api.Update.parse_updateReadStories($0) } dict[-145845461] = { return Api.Update.parse_updateReadStories($0) }
dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) } dict[821314523] = { return Api.Update.parse_updateRecentEmojiStatuses($0) }
dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) } dict[1870160884] = { return Api.Update.parse_updateRecentReactions($0) }

View File

@ -1165,7 +1165,7 @@ public extension Api {
case updateReadFeaturedStickers case updateReadFeaturedStickers
case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32) case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32)
case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32) case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32)
case updateReadMessagesContents(messages: [Int32], pts: Int32, ptsCount: Int32) case updateReadMessagesContents(flags: Int32, messages: [Int32], pts: Int32, ptsCount: Int32, date: Int32?)
case updateReadStories(peer: Api.Peer, maxId: Int32) case updateReadStories(peer: Api.Peer, maxId: Int32)
case updateRecentEmojiStatuses case updateRecentEmojiStatuses
case updateRecentReactions case updateRecentReactions
@ -2007,10 +2007,11 @@ public extension Api {
serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(pts, buffer: buffer, boxed: false)
serializeInt32(ptsCount, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false)
break break
case .updateReadMessagesContents(let messages, let pts, let ptsCount): case .updateReadMessagesContents(let flags, let messages, let pts, let ptsCount, let date):
if boxed { if boxed {
buffer.appendInt32(1757493555) buffer.appendInt32(-131960447)
} }
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(messages.count)) buffer.appendInt32(Int32(messages.count))
for item in messages { for item in messages {
@ -2018,6 +2019,7 @@ public extension Api {
} }
serializeInt32(pts, buffer: buffer, boxed: false) serializeInt32(pts, buffer: buffer, boxed: false)
serializeInt32(ptsCount, buffer: buffer, boxed: false) serializeInt32(ptsCount, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(date!, buffer: buffer, boxed: false)}
break break
case .updateReadStories(let peer, let maxId): case .updateReadStories(let peer, let maxId):
if boxed { if boxed {
@ -2384,8 +2386,8 @@ public extension Api {
return ("updateReadHistoryInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) return ("updateReadHistoryInbox", [("flags", flags as Any), ("folderId", folderId as Any), ("peer", peer as Any), ("maxId", maxId as Any), ("stillUnreadCount", stillUnreadCount as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)])
case .updateReadHistoryOutbox(let peer, let maxId, let pts, let ptsCount): case .updateReadHistoryOutbox(let peer, let maxId, let pts, let ptsCount):
return ("updateReadHistoryOutbox", [("peer", peer as Any), ("maxId", maxId as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) return ("updateReadHistoryOutbox", [("peer", peer as Any), ("maxId", maxId as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)])
case .updateReadMessagesContents(let messages, let pts, let ptsCount): case .updateReadMessagesContents(let flags, let messages, let pts, let ptsCount, let date):
return ("updateReadMessagesContents", [("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any)]) return ("updateReadMessagesContents", [("flags", flags as Any), ("messages", messages as Any), ("pts", pts as Any), ("ptsCount", ptsCount as Any), ("date", date as Any)])
case .updateReadStories(let peer, let maxId): case .updateReadStories(let peer, let maxId):
return ("updateReadStories", [("peer", peer as Any), ("maxId", maxId as Any)]) return ("updateReadStories", [("peer", peer as Any), ("maxId", maxId as Any)])
case .updateRecentEmojiStatuses: case .updateRecentEmojiStatuses:
@ -4116,19 +4118,25 @@ public extension Api {
} }
} }
public static func parse_updateReadMessagesContents(_ reader: BufferReader) -> Update? { public static func parse_updateReadMessagesContents(_ reader: BufferReader) -> Update? {
var _1: [Int32]? var _1: Int32?
_1 = reader.readInt32()
var _2: [Int32]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
} }
var _2: Int32?
_2 = reader.readInt32()
var _3: Int32? var _3: Int32?
_3 = reader.readInt32() _3 = reader.readInt32()
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_5 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = _4 != nil
return Api.Update.updateReadMessagesContents(messages: _1!, pts: _2!, ptsCount: _3!) let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.Update.updateReadMessagesContents(flags: _1!, messages: _2!, pts: _3!, ptsCount: _4!, date: _5)
} }
else { else {
return nil return nil

View File

@ -522,7 +522,7 @@ public extension Api {
} }
public extension Api { public extension Api {
indirect enum Chat: TypeConstructorDescription { indirect enum Chat: TypeConstructorDescription {
case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?) case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?)
case channelForbidden(flags: Int32, id: Int64, accessHash: Int64, title: String, untilDate: Int32?) case channelForbidden(flags: Int32, id: Int64, accessHash: Int64, title: String, untilDate: Int32?)
case chat(flags: Int32, id: Int64, title: String, photo: Api.ChatPhoto, participantsCount: Int32, date: Int32, version: Int32, migratedTo: Api.InputChannel?, adminRights: Api.ChatAdminRights?, defaultBannedRights: Api.ChatBannedRights?) case chat(flags: Int32, id: Int64, title: String, photo: Api.ChatPhoto, participantsCount: Int32, date: Int32, version: Int32, migratedTo: Api.InputChannel?, adminRights: Api.ChatAdminRights?, defaultBannedRights: Api.ChatBannedRights?)
case chatEmpty(id: Int64) case chatEmpty(id: Int64)
@ -530,9 +530,9 @@ public extension Api {
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames): case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId):
if boxed { if boxed {
buffer.appendInt32(-2094689180) buffer.appendInt32(-1795845413)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(flags2, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false)
@ -556,6 +556,7 @@ public extension Api {
for item in usernames! { for item in usernames! {
item.serialize(buffer, true) item.serialize(buffer, true)
}} }}
if Int(flags2) & Int(1 << 4) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)}
break break
case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate):
if boxed { if boxed {
@ -600,8 +601,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames): case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId):
return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any)]) return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any)])
case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate):
return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)]) return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)])
case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights): case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights):
@ -654,6 +655,8 @@ public extension Api {
if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
_14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self) _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self)
} } } }
var _15: Int32?
if Int(_2!) & Int(1 << 4) != 0 {_15 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -668,8 +671,9 @@ public extension Api {
let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil
let _c13 = (Int(_1!) & Int(1 << 17) == 0) || _13 != nil let _c13 = (Int(_1!) & Int(1 << 17) == 0) || _13 != nil
let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { let _c15 = (Int(_2!) & Int(1 << 4) == 0) || _15 != nil
return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 {
return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15)
} }
else { else {
return nil return nil

View File

@ -124,6 +124,7 @@ enum AccountStateMutationOperation {
case UpdateReadStories(peerId: PeerId, maxId: Int32) case UpdateReadStories(peerId: PeerId, maxId: Int32)
case UpdateStoryStealthMode(data: Api.StoriesStealthMode) case UpdateStoryStealthMode(data: Api.StoriesStealthMode)
case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction)
case UpdateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String)
} }
struct HoleFromPreviousState { struct HoleFromPreviousState {
@ -467,7 +468,7 @@ struct AccountMutableState {
for chat in chats { for chat in chats {
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _):
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
self.addOperation(.UpdateCachedPeerData(chat.peerId, { current in self.addOperation(.UpdateCachedPeerData(chat.peerId, { current in
var previous: CachedChannelData var previous: CachedChannelData
@ -653,9 +654,13 @@ struct AccountMutableState {
self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction)) self.addOperation(.UpdateStorySentReaction(peerId: peerId, id: id, reaction: reaction))
} }
mutating func updateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String) {
self.addOperation(.UpdateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date, device: device, location: location))
}
mutating func addOperation(_ operation: AccountStateMutationOperation) { mutating func addOperation(_ operation: AccountStateMutationOperation) {
switch operation { switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization:
break break
case let .AddMessages(messages, location): case let .AddMessages(messages, location):
for message in messages { for message in messages {

View File

@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? {
return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
case let .chatForbidden(id, title): case let .chatForbidden(id, title):
return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames): case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _):
let isMin = (flags & (1 << 12)) != 0 let isMin = (flags & (1 << 12)) != 0
let participationStatus: TelegramChannelParticipationStatus let participationStatus: TelegramChannelParticipationStatus
@ -170,7 +170,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? {
switch rhs { switch rhs {
case .chat, .chatEmpty, .chatForbidden, .channelForbidden: case .chat, .chatEmpty, .chatForbidden, .channelForbidden:
return parseTelegramGroupOrChannel(chat: rhs) return parseTelegramGroupOrChannel(chat: rhs)
case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames): case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _):
let isMin = (flags & (1 << 12)) != 0 let isMin = (flags & (1 << 12)) != 0
if accessHash != nil && !isMin { if accessHash != nil && !isMin {
return parseTelegramGroupOrChannel(chat: rhs) return parseTelegramGroupOrChannel(chat: rhs)

View File

@ -18,6 +18,9 @@ public enum TelegramChannelPermission {
case changeInfo case changeInfo
case canBeAnonymous case canBeAnonymous
case manageCalls case manageCalls
case postStories
case editStories
case deleteStories
} }
public extension TelegramChannel { public extension TelegramChannel {
@ -235,6 +238,24 @@ public extension TelegramChannel {
return true return true
} }
return false return false
case .postStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostStories)
} else {
return false
}
case .editStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canEditStories)
} else {
return false
}
case .deleteStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canDeleteStories)
} else {
return false
}
} }
} }

View File

@ -1479,7 +1479,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
case let .updateChannelPinnedTopic(flags, channelId, topicId): case let .updateChannelPinnedTopic(flags, channelId, topicId):
let isPinned = (flags & (1 << 0)) != 0 let isPinned = (flags & (1 << 0)) != 0
updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned) updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned)
case let .updateReadMessagesContents(messages, _, _): case let .updateReadMessagesContents(_, messages, _, _, _):
updatedState.addReadMessagesContents((nil, messages)) updatedState.addReadMessagesContents((nil, messages))
case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages): case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages):
let _ = topMsgId let _ = topMsgId
@ -1681,6 +1681,9 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
updatedState.updateStoryStealthMode(stealthMode) updatedState.updateStoryStealthMode(stealthMode)
case let .updateSentStoryReaction(peerId, storyId, reaction): case let .updateSentStoryReaction(peerId, storyId, reaction):
updatedState.updateStorySentReaction(peerId: peerId.peerId, id: storyId, reaction: reaction) updatedState.updateStorySentReaction(peerId: peerId.peerId, id: storyId, reaction: reaction)
case let .updateNewAuthorization(flags, hash, date, device, location):
let isUnconfirmed = (flags & (1 << 0)) != 0
updatedState.updateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date ?? 0, device: device ?? "", location: location ?? "")
default: default:
break break
} }
@ -3169,7 +3172,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddScheduledMessages: OptimizeAddMessagesState? var currentAddScheduledMessages: OptimizeAddMessagesState?
for operation in operations { for operation in operations {
switch operation { switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction: case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization:
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
} }
@ -4586,6 +4589,7 @@ func replayFinalState(
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: updatedReaction myReaction: updatedReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
@ -4616,6 +4620,7 @@ func replayFinalState(
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: MessageReaction.Reaction(apiReaction: reaction) myReaction: MessageReaction.Reaction(apiReaction: reaction)
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
@ -4623,6 +4628,20 @@ func replayFinalState(
storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: updatedItem)) storyUpdates.append(InternalStoryUpdate.added(peerId: peerId, item: updatedItem))
} }
} }
case let .UpdateNewAuthorization(isUnconfirmed, hash, date, device, location):
let id = NewSessionReview.Id(id: hash)
if isUnconfirmed {
if let entry = CodableEntry(NewSessionReview(
id: hash,
device: device,
location: location,
timestamp: date
)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, item: OrderedItemListEntry(id: id.rawValue, contents: entry), removeTailIfCountExceeds: 200)
}
} else {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: id.rawValue)
}
} }
} }

View File

@ -7,6 +7,7 @@ import MtProtoKit
struct AccumulatedPeers { struct AccumulatedPeers {
var peers: [PeerId: Peer] = [:] var peers: [PeerId: Peer] = [:]
var users: [PeerId: Api.User] = [:] var users: [PeerId: Api.User] = [:]
var chats: [PeerId: Api.Chat] = [:]
var allIds: Set<PeerId> { var allIds: Set<PeerId> {
var result = Set<PeerId>() var result = Set<PeerId>()
@ -31,6 +32,9 @@ struct AccumulatedPeers {
for user in users { for user in users {
self.users[user.peerId] = user self.users[user.peerId] = user
} }
for chat in chats {
self.chats[chat.peerId] = chat
}
} }
init(chats: [Api.Chat], users: [Api.User]) { init(chats: [Api.Chat], users: [Api.User]) {
@ -42,6 +46,9 @@ struct AccumulatedPeers {
for user in users { for user in users {
self.users[user.peerId] = user self.users[user.peerId] = user
} }
for chat in chats {
self.chats[chat.peerId] = chat
}
} }
init(users: [Api.User]) { init(users: [Api.User]) {
@ -65,6 +72,9 @@ struct AccumulatedPeers {
for (id, user) in other.users { for (id, user) in other.users {
result.users[id] = user result.users[id] = user
} }
for (id, chat) in other.chats {
result.chats[id] = chat
}
return result return result
} }

View File

@ -70,7 +70,7 @@ func apiUpdatePtsRange(_ update: Api.Update) -> (Int32, Int32)? {
return (pts, ptsCount) return (pts, ptsCount)
case let .updateEditMessage(_, pts, ptsCount): case let .updateEditMessage(_, pts, ptsCount):
return (pts, ptsCount) return (pts, ptsCount)
case let .updateReadMessagesContents(_, pts, ptsCount): case let .updateReadMessagesContents(_, _, pts, ptsCount, _):
return (pts, ptsCount) return (pts, ptsCount)
case let .updateWebPage(_, pts, ptsCount): case let .updateWebPage(_, pts, ptsCount):
return (pts, ptsCount) return (pts, ptsCount)

View File

@ -181,7 +181,7 @@ extension Api.Chat {
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .chatForbidden(id, _): case let .chatForbidden(id, _):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _): case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))
case let .channelForbidden(_, id, _, _, _): case let .channelForbidden(_, id, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import Postbox import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramApi
public final class NewSessionReview: Codable, Equatable { public final class NewSessionReview: Codable, Equatable {
struct Id { struct Id {
@ -19,11 +20,13 @@ public final class NewSessionReview: Codable, Equatable {
public let id: Int64 public let id: Int64
public let device: String public let device: String
public let location: String public let location: String
public let timestamp: Int32
public init(id: Int64, device: String, location: String) { public init(id: Int64, device: String, location: String, timestamp: Int32) {
self.id = id self.id = id
self.device = device self.device = device
self.location = location self.location = location
self.timestamp = timestamp
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -32,6 +35,7 @@ public final class NewSessionReview: Codable, Equatable {
self.id = try container.decode(Int64.self, forKey: "id") self.id = try container.decode(Int64.self, forKey: "id")
self.device = try container.decode(String.self, forKey: "device") self.device = try container.decode(String.self, forKey: "device")
self.location = try container.decode(String.self, forKey: "location") self.location = try container.decode(String.self, forKey: "location")
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -40,6 +44,7 @@ public final class NewSessionReview: Codable, Equatable {
try container.encode(self.id, forKey: "id") try container.encode(self.id, forKey: "id")
try container.encode(self.device, forKey: "device") try container.encode(self.device, forKey: "device")
try container.encode(self.location, forKey: "location") try container.encode(self.location, forKey: "location")
try container.encode(self.timestamp, forKey: "timestamp")
} }
public static func ==(lhs: NewSessionReview, rhs: NewSessionReview) -> Bool { public static func ==(lhs: NewSessionReview, rhs: NewSessionReview) -> Bool {
@ -52,10 +57,42 @@ public final class NewSessionReview: Codable, Equatable {
if lhs.location != rhs.location { if lhs.location != rhs.location {
return false return false
} }
if lhs.timestamp != rhs.timestamp {
return false
}
return true return true
} }
} }
func _internal_cleanupSessionReviews(account: Account) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
var autoconfirmTimeout: Int32 = 7 * 24 * 60 * 60
let appConfig = currentAppConfiguration(transaction: transaction)
if let data = appConfig.data {
if let value = data["authorization_autoconfirm_period"] as? Double {
autoconfirmTimeout = Int32(round(value))
}
}
let timestamp = Int32(Date().timeIntervalSince1970)
var removeIds: [MemoryBuffer] = []
for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.NewSessionReviews) {
guard let item = entry.contents.get(NewSessionReview.self) else {
removeIds.append(entry.id)
continue
}
if item.timestamp <= timestamp - autoconfirmTimeout {
removeIds.append(entry.id)
}
}
for removeId in removeIds {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: removeId)
}
}
|> ignoreValues
}
public func newSessionReviews(postbox: Postbox) -> Signal<[NewSessionReview], NoError> { public func newSessionReviews(postbox: Postbox) -> Signal<[NewSessionReview], NoError> {
let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.NewSessionReviews) let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.NewSessionReviews)
return postbox.combinedView(keys: [viewKey]) return postbox.combinedView(keys: [viewKey])
@ -102,3 +139,11 @@ public func removeNewSessionReviews(postbox: Postbox, ids: [Int64]) -> Signal<Ne
} }
|> ignoreValues |> ignoreValues
} }
func _internal_confirmNewSessionReview(account: Account, id: Int64) -> Signal<Never, NoError> {
return account.network.request(Api.functions.account.changeAuthorizationSettings(flags: 1 << 3, hash: id, encryptedRequestsDisabled: nil, callRequestsDisabled: nil))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}

View File

@ -22,13 +22,17 @@ public struct TelegramChatAdminRightsFlags: OptionSet, Hashable {
public static let canBeAnonymous = TelegramChatAdminRightsFlags(rawValue: 1 << 10) public static let canBeAnonymous = TelegramChatAdminRightsFlags(rawValue: 1 << 10)
public static let canManageCalls = TelegramChatAdminRightsFlags(rawValue: 1 << 11) public static let canManageCalls = TelegramChatAdminRightsFlags(rawValue: 1 << 11)
public static let canManageTopics = TelegramChatAdminRightsFlags(rawValue: 1 << 13) public static let canManageTopics = TelegramChatAdminRightsFlags(rawValue: 1 << 13)
public static let canPostStories = TelegramChatAdminRightsFlags(rawValue: 1 << 14)
public static let canEditStories = TelegramChatAdminRightsFlags(rawValue: 1 << 15)
public static let canDeleteStories = TelegramChatAdminRightsFlags(rawValue: 1 << 16)
public static var all: TelegramChatAdminRightsFlags { public static var all: TelegramChatAdminRightsFlags {
return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canBeAnonymous, .canManageCalls, .canManageTopics] return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canBeAnonymous, .canManageCalls, .canManageTopics, .canPostStories, .canEditStories, .canDeleteStories]
} }
public static var allChannel: TelegramChatAdminRightsFlags { public static var allChannel: TelegramChatAdminRightsFlags {
return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canManageCalls, .canManageTopics] return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canManageCalls, .canManageTopics, .canPostStories, .canEditStories, .canDeleteStories]
} }
public static let internal_groupSpecific: TelegramChatAdminRightsFlags = [ public static let internal_groupSpecific: TelegramChatAdminRightsFlags = [
@ -49,7 +53,10 @@ public struct TelegramChatAdminRightsFlags: OptionSet, Hashable {
.canDeleteMessages, .canDeleteMessages,
.canManageCalls, .canManageCalls,
.canInviteUsers, .canInviteUsers,
.canAddAdmins .canAddAdmins,
.canPostStories,
.canEditStories,
.canDeleteStories
] ]
public static func peerSpecific(peer: EnginePeer) -> TelegramChatAdminRightsFlags { public static func peerSpecific(peer: EnginePeer) -> TelegramChatAdminRightsFlags {

View File

@ -2254,7 +2254,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network:
for chat in chats { for chat in chats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _):
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount subscribers[groupOrChannel.id] = participantsCount
} }

View File

@ -375,6 +375,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
@ -411,6 +412,7 @@ public final class EngineStoryViewListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {

View File

@ -138,7 +138,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network,
for chat in chats { for chat in chats {
if let groupOrChannel = parsedPeers.get(chat.peerId) { if let groupOrChannel = parsedPeers.get(chat.peerId) {
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _):
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount subscribers[groupOrChannel.id] = participantsCount
} }

View File

@ -180,6 +180,7 @@ public enum Stories {
case isSelectedContacts case isSelectedContacts
case isForwardingDisabled case isForwardingDisabled
case isEdited case isEdited
case isMy
case myReaction case myReaction
} }
@ -200,6 +201,7 @@ public enum Stories {
public let isSelectedContacts: Bool public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: Bool public let isEdited: Bool
public let isMy: Bool
public let myReaction: MessageReaction.Reaction? public let myReaction: MessageReaction.Reaction?
public init( public init(
@ -220,6 +222,7 @@ public enum Stories {
isSelectedContacts: Bool, isSelectedContacts: Bool,
isForwardingDisabled: Bool, isForwardingDisabled: Bool,
isEdited: Bool, isEdited: Bool,
isMy: Bool,
myReaction: MessageReaction.Reaction? myReaction: MessageReaction.Reaction?
) { ) {
self.id = id self.id = id
@ -239,6 +242,7 @@ public enum Stories {
self.isSelectedContacts = isSelectedContacts self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited self.isEdited = isEdited
self.isMy = isMy
self.myReaction = myReaction self.myReaction = myReaction
} }
@ -268,6 +272,7 @@ public enum Stories {
self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false
self.isMy = try container.decodeIfPresent(Bool.self, forKey: .isMy) ?? false
self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction)
} }
@ -298,6 +303,7 @@ public enum Stories {
try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts) try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.isEdited, forKey: .isEdited) try container.encode(self.isEdited, forKey: .isEdited)
try container.encode(self.isMy, forKey: .isMy)
try container.encodeIfPresent(self.myReaction, forKey: .myReaction) try container.encodeIfPresent(self.myReaction, forKey: .myReaction)
} }
@ -1025,6 +1031,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
@ -1204,6 +1211,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
@ -1232,6 +1240,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
@ -1406,6 +1415,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
@ -1433,6 +1443,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
updatedItems.append(updatedItem) updatedItems.append(updatedItem)
@ -1575,6 +1586,13 @@ extension Stories.StoredItem {
mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:)) mergedMyReaction = sentReaction.flatMap(MessageReaction.Reaction.init(apiReaction:))
} }
var mergedIsMy: Bool
if isMin, let existingItem = existingItem {
mergedIsMy = existingItem.isMy
} else {
mergedIsMy = (flags & (1 << 16)) != 0
}
let item = Stories.Item( let item = Stories.Item(
id: id, id: id,
timestamp: date, timestamp: date,
@ -1593,6 +1611,7 @@ extension Stories.StoredItem {
isSelectedContacts: isSelectedContacts, isSelectedContacts: isSelectedContacts,
isForwardingDisabled: isForwardingDisabled, isForwardingDisabled: isForwardingDisabled,
isEdited: isEdited, isEdited: isEdited,
isMy: mergedIsMy,
myReaction: mergedMyReaction myReaction: mergedMyReaction
) )
self = .item(item) self = .item(item)
@ -2046,6 +2065,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: reaction myReaction: reaction
)) ))
updatedItemValue = updatedItem updatedItemValue = updatedItem
@ -2076,6 +2096,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: reaction myReaction: reaction
)) ))
updatedItemValue = updatedItem updatedItemValue = updatedItem

View File

@ -69,9 +69,10 @@ public final class EngineStoryItem: Equatable {
public let isSelectedContacts: Bool public let isSelectedContacts: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: Bool public let isEdited: Bool
public let isMy: Bool
public let myReaction: MessageReaction.Reaction? public let myReaction: MessageReaction.Reaction?
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, myReaction: MessageReaction.Reaction?) { public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?) {
self.id = id self.id = id
self.timestamp = timestamp self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp self.expirationTimestamp = expirationTimestamp
@ -90,6 +91,7 @@ public final class EngineStoryItem: Equatable {
self.isSelectedContacts = isSelectedContacts self.isSelectedContacts = isSelectedContacts
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited self.isEdited = isEdited
self.isMy = isMy
self.myReaction = myReaction self.myReaction = myReaction
} }
@ -148,6 +150,9 @@ public final class EngineStoryItem: Equatable {
if lhs.isEdited != rhs.isEdited { if lhs.isEdited != rhs.isEdited {
return false return false
} }
if lhs.isMy != rhs.isMy {
return false
}
if lhs.myReaction != rhs.myReaction { if lhs.myReaction != rhs.myReaction {
return false return false
} }
@ -189,6 +194,7 @@ extension EngineStoryItem {
isSelectedContacts: self.isSelectedContacts, isSelectedContacts: self.isSelectedContacts,
isForwardingDisabled: self.isForwardingDisabled, isForwardingDisabled: self.isForwardingDisabled,
isEdited: self.isEdited, isEdited: self.isEdited,
isMy: self.isMy,
myReaction: self.myReaction myReaction: self.myReaction
) )
} }
@ -563,6 +569,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
items.append(mappedItem) items.append(mappedItem)
@ -693,6 +700,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
storyItems.append(mappedItem) storyItems.append(mappedItem)
@ -847,6 +855,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
@ -892,6 +901,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
@ -939,6 +949,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
@ -982,6 +993,7 @@ public final class PeerStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
@ -1149,6 +1161,7 @@ public final class PeerExpiringStoryListContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
items.append(.item(mappedItem)) items.append(.item(mappedItem))

View File

@ -1132,6 +1132,7 @@ public extension TelegramEngine {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {

View File

@ -280,7 +280,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
var memberCounts: [PeerId: Int] = [:] var memberCounts: [PeerId: Int] = [:]
for chat in chats { for chat in chats {
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat { if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat {
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount) memberCounts[chat.peerId] = Int(participantsCount)
} }
@ -307,7 +307,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
var memberCounts: [PeerId: Int] = [:] var memberCounts: [PeerId: Int] = [:]
for chat in chats { for chat in chats {
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat { if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat {
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount) memberCounts[chat.peerId] = Int(participantsCount)
} }
@ -621,7 +621,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = [] var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = []
for chat in chats { for chat in chats {
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat { if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _) = chat {
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount)) memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount))
} }

View File

@ -31,7 +31,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel]
var participantsCounts: [PeerId: Int32] = [:] var participantsCounts: [PeerId: Int32] = [:]
for chat in chats { for chat in chats {
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _):
if let participantsCountValue = participantsCountValue { if let participantsCountValue = participantsCountValue {
participantsCounts[chat.peerId] = participantsCountValue participantsCounts[chat.peerId] = participantsCountValue
} }

View File

@ -38,7 +38,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
for chat in chats { for chat in chats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _):
if let participantsCount = participantsCount { if let participantsCount = participantsCount {
subscribers[groupOrChannel.id] = participantsCount subscribers[groupOrChannel.id] = participantsCount
} }

View File

@ -820,7 +820,7 @@ public extension TelegramEngine {
|> beforeNext { _ in |> beforeNext { _ in
let delayTime = CFAbsoluteTimeGetCurrent() - startTime let delayTime = CFAbsoluteTimeGetCurrent() - startTime
if delayTime > 0.3 { if delayTime > 0.3 {
Logger.shared.log("getNextUnreadChannel", "took \(delayTime) s") //Logger.shared.log("getNextUnreadChannel", "took \(delayTime) s")
} }
} }
} }

View File

@ -68,5 +68,21 @@ public extension TelegramEngine {
public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> { public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
return _internal_updateCloseFriends(account: self.account, peerIds: peerIds) return _internal_updateCloseFriends(account: self.account, peerIds: peerIds)
} }
public func cleanupSessionReviews() -> Signal<Never, NoError> {
return _internal_cleanupSessionReviews(account: self.account)
}
public func confirmNewSessionReview(id: Int64) -> Signal<Never, NoError> {
let _ = removeNewSessionReviews(postbox: self.account.postbox, ids: [id]).start()
return _internal_confirmNewSessionReview(account: self.account, id: id)
}
public func terminateAnotherSession(id: Int64) -> Signal<Never, TerminateSessionError> {
let _ = removeNewSessionReviews(postbox: self.account.postbox, ids: [id]).start()
return terminateAccountSession(account: self.account, hash: id)
|> ignoreValues
}
} }
} }

View File

@ -166,7 +166,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal<Account
if let peer = parseTelegramGroupOrChannel(chat: chat) { if let peer = parseTelegramGroupOrChannel(chat: chat) {
var participantCount: Int32? = nil var participantCount: Int32? = nil
switch chat { switch chat {
case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _): case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _):
participantCount = participantsCountValue participantCount = participantsCountValue
default: default:
break break

View File

@ -70,6 +70,21 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
} }
} }
} }
for (_, chat) in peers.chats {
switch chat {
case let .channel(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId):
let isMin = (flags & (1 << 12)) != 0
let storiesUnavailable = (flags2 & (1 << 3)) != 0
if let storiesMaxId = storiesMaxId {
transaction.setStoryItemsInexactMaxId(peerId: chat.peerId, id: storiesMaxId)
} else if !isMin && storiesUnavailable {
transaction.clearStoryItemsInexactMaxId(peerId: chat.peerId)
}
default:
break
}
}
for (_, peer) in peers.peers { for (_, peer) in peers.peers {
parsedPeers.append(peer) parsedPeers.append(peer)
} }

View File

@ -2809,7 +2809,11 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
if let accountPeer { if let accountPeer {
sendAsPeers.append(accountPeer) sendAsPeers.append(accountPeer)
} }
sendAsPeers.append(contentsOf: adminedChannels) for channel in adminedChannels {
if case let .channel(channel) = channel, channel.hasPermission(.postStories) {
sendAsPeers.append(contentsOf: adminedChannels)
}
}
let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers
var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:] var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:]

View File

@ -3241,6 +3241,10 @@ final class StorageUsageScreenComponent: Component {
} }
} }
if case .separator = subItems.last {
subItems.removeLast()
}
if let sourceLabelView = sourceView.labelView { if let sourceLabelView = sourceView.labelView {
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems))) let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView)) let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView))

View File

@ -187,6 +187,7 @@ public final class StoryContentContextImpl: StoryContentContext {
isSelectedContacts: item.isSelectedContacts, isSelectedContacts: item.isSelectedContacts,
isForwardingDisabled: item.isForwardingDisabled, isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited, isEdited: item.isEdited,
isMy: item.isMy,
myReaction: item.myReaction myReaction: item.myReaction
) )
} }
@ -220,6 +221,7 @@ public final class StoryContentContextImpl: StoryContentContext {
isSelectedContacts: item.privacy.base == .nobody, isSelectedContacts: item.privacy.base == .nobody,
isForwardingDisabled: false, isForwardingDisabled: false,
isEdited: false, isEdited: false,
isMy: true,
myReaction: nil myReaction: nil
)) ))
totalCount += 1 totalCount += 1
@ -1096,6 +1098,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
isSelectedContacts: itemValue.isSelectedContacts, isSelectedContacts: itemValue.isSelectedContacts,
isForwardingDisabled: itemValue.isForwardingDisabled, isForwardingDisabled: itemValue.isForwardingDisabled,
isEdited: itemValue.isEdited, isEdited: itemValue.isEdited,
isMy: itemValue.isMy,
myReaction: itemValue.myReaction myReaction: itemValue.myReaction
) )

View File

@ -5679,59 +5679,68 @@ public final class StoryItemSetContainerComponent: Component {
guard let component = self.component, let controller = component.controller() else { guard let component = self.component, let controller = component.controller() else {
return return
} }
guard case let .channel(channel) = component.slice.peer else {
return
}
self.dismissAllTooltips() self.dismissAllTooltips()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.editStories) {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in
}, action: { [weak self] _, a in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
a(.default) }, action: { [weak self] _, a in
a(.default)
guard let self else {
return guard let self else {
} return
self.openStoryEditing() }
}))) self.openStoryEditing()
})))
}
items.append(.separator) if !items.isEmpty {
items.append(.separator)
}
//TODO:localize if channel.hasPermission(.editStories) {
items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from Posts" : "Save to Posts", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
//TODO:localize //TODO:localize
if component.slice.item.storyItem.isPinned { items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from Posts" : "Save to Posts", icon: { theme in
self.component?.presentController(UndoOverlayController( return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Stories/Context Menu/Unpin" : "Stories/Context Menu/Pin"), color: theme.contextMenu.primaryColor)
presentationData: presentationData, }, action: { [weak self] _, a in
content: .info(title: nil, text: "Story removed from the channel's profile", timeout: nil), a(.default)
elevatedLayout: false,
animateInAsReplacement: false, guard let self, let component = self.component else {
blurred: true, return
action: { _ in return false } }
), nil)
} else { let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.slice.peer.id, ids: [component.slice.item.storyItem.id: component.slice.item.storyItem], isPinned: !component.slice.item.storyItem.isPinned).start()
self.component?.presentController(UndoOverlayController(
presentationData: presentationData, let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
content: .info(title: "Story saved to the channel's profile", text: "Saved stories can be viewed by others on the channel's profile until an admin removes them.", timeout: nil), //TODO:localize
elevatedLayout: false, if component.slice.item.storyItem.isPinned {
animateInAsReplacement: false, self.component?.presentController(UndoOverlayController(
blurred: true, presentationData: presentationData,
action: { _ in return false } content: .info(title: nil, text: "Story removed from the channel's profile", timeout: nil),
), nil) elevatedLayout: false,
} animateInAsReplacement: false,
}))) blurred: true,
action: { _ in return false }
), nil)
} else {
self.component?.presentController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: "Story saved to the channel's profile", text: "Saved stories can be viewed by others on the channel's profile until an admin removes them.", timeout: nil),
elevatedLayout: false,
animateInAsReplacement: false,
blurred: true,
action: { _ in return false }
), nil)
}
})))
}
let saveText: String = component.strings.Story_Context_SaveToGallery let saveText: String = component.strings.Story_Context_SaveToGallery
items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in
@ -5745,23 +5754,6 @@ public final class StoryItemSetContainerComponent: Component {
self.requestSave() self.requestSave()
}))) })))
if case let .user(accountUser) = component.slice.peer {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self else {
return
}
if accountUser.isPremium {
self.sendMessageContext.requestStealthMode(view: self)
} else {
self.presentStealthModeUpgradeScreen()
}
})))
}
if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { if component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
@ -5773,7 +5765,7 @@ public final class StoryItemSetContainerComponent: Component {
} }
let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id) let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)
|> deliverOnMainQueue).start(next: { [weak self] link in |> deliverOnMainQueue).start(next: { [weak self] link in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
@ -5803,48 +5795,50 @@ public final class StoryItemSetContainerComponent: Component {
}))) })))
} }
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in if (component.slice.item.storyItem.isMy && channel.hasPermission(.postStories)) || channel.hasPermission(.deleteStories) {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteStory, textColor: .destructive, icon: { theme in
}, action: { [weak self] _, a in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
a(.default) }, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return guard let self, let component = self.component else {
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: component.strings.Story_ContextDeleteStory, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
component.delete()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
actionSheet.dismissed = { [weak self] _ in
guard let self else {
return return
} }
self.sendMessageContext.actionSheet = nil
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: component.strings.Story_ContextDeleteStory, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component else {
return
}
component.delete()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
actionSheet.dismissed = { [weak self] _ in
guard let self else {
return
}
self.sendMessageContext.actionSheet = nil
self.updateIsProgressPaused()
}
self.sendMessageContext.actionSheet = actionSheet
self.updateIsProgressPaused() self.updateIsProgressPaused()
}
self.sendMessageContext.actionSheet = actionSheet component.presentController(actionSheet, nil)
self.updateIsProgressPaused() })))
}
component.presentController(actionSheet, nil)
})))
let (tip, tipSignal) = self.getLinkedStickerPacks() let (tip, tipSignal) = self.getLinkedStickerPacks()

View File

@ -5321,10 +5321,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
} else if let channel = peer as? TelegramChannel { } else if let channel = peer as? TelegramChannel {
if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData {
if channel.hasPermission(.sendSomething) { if channel.hasPermission(.editStories) {
//TODO:localize //TODO:localize
items.append(.action(ContextMenuActionItem(text: "Archived Stories", icon: { theme in items.append(.action(ContextMenuActionItem(text: "Archived Stories", icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor) generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)