Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2019-12-16 21:56:10 +04:00
commit fccae60f77
25 changed files with 5126 additions and 4117 deletions

View File

@ -5193,3 +5193,25 @@ Any member of this group will be able to see messages in the channel.";
"WallpaperPreview.PatternPaternApply" = "Apply";
"ChatContextMenu.TextSelectionTip" = "Hold a word, then move cursor to select more| text to copy.";
"OldChannels.Title" = "Limit Reached";
"OldChannels.NoticeTitle" = "Too Many Groups and Channels";
"OldChannels.NoticeText" = "Sorry, you are member of too many groups and channels.\nPlease leave some before joining new one.";
"OldChannels.ChannelsHeader" = "MOST INACTIVE";
"OldChannels.Leave" = "Leave";
"OldChannels.ChannelFormat" = "channel, ";
"OldChannels.GroupEmptyFormat" = "group, ";
"OldChannels.GroupFormat_1" = "%@ member";
"OldChannels.GroupFormat_any" = "%@ members";
"OldChannels.InactiveWeek_1" = "inactive %@ week";
"OldChannels.InactiveWeek_any" = "inactive %@ weeks";
"OldChannels.InactiveMonth_1" = "inactive %@ month";
"OldChannels.InactiveMonth_any" = "inactive %@ months";
"OldChannels.InactiveYear_1" = "inactive %@ year";
"OldChannels.InactiveYear_any" = "inactive %@ years";
"PrivacySettings.WebSessions" = "Active Websites";

View File

@ -283,7 +283,7 @@ public final class AvatarNode: ASDisplayNode {
self.imageNode.isHidden = true
}
public func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false) {
public func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) {
var synchronousLoad = synchronousLoad
var representation: TelegramMediaImageRepresentation?
var icon = AvatarNodeIcon.none
@ -318,7 +318,7 @@ public final class AvatarNode: ASDisplayNode {
let parameters: AvatarNodeParameters
if let peer = peer, let signal = peerAvatarImage(account: context.account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, emptyColor: emptyColor, synchronousLoad: synchronousLoad) {
if let peer = peer, let signal = peerAvatarImage(account: context.account, peer: peer, authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, emptyColor: emptyColor, synchronousLoad: synchronousLoad) {
self.contents = nil
self.displaySuspended = true
self.imageReady.set(self.imageNode.ready)

View File

@ -87,7 +87,7 @@ public final class BotCheckoutController: ViewController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, additionalInsets: UIEdgeInsets())
}
@objc private func cancelPressed() {

View File

@ -705,10 +705,10 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition)
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)

View File

@ -81,7 +81,7 @@ public final class BotReceiptController: ViewController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, additionalInsets: UIEdgeInsets())
}
@objc private func cancelPressed() {

View File

@ -301,10 +301,10 @@ final class BotReceiptControllerNode: ItemListControllerNode {
self.dataRequestDisposable?.dispose()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var updatedInsets = layout.intrinsicInsets
updatedInsets.bottom += BotCheckoutActionButton.diameter + 20.0
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition)
super.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: updatedInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationBarHeight, transition: transition, additionalInsets: additionalInsets)
let actionButtonFrame = CGRect(origin: CGPoint(x: 10.0, y: layout.size.height - 10.0 - BotCheckoutActionButton.diameter - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width - 20.0, height: BotCheckoutActionButton.diameter))
transition.updateFrame(node: self.actionButton, frame: actionButtonFrame)

View File

@ -547,7 +547,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0))
}
self.contextContainer.isGestureEnabled = enablePreview

View File

@ -105,8 +105,10 @@ public enum ContactsPeerItemPeer: Equatable {
}
}
public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader {
public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let presentationData: ItemListPresentationData
let style: ItemListStyle
public let sectionId: ItemListSectionId
let sortOrder: PresentationPersonNameOrder
let displayOrder: PresentationPersonNameOrder
let context: AccountContext
@ -131,8 +133,10 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader {
public let header: ListViewItemHeader?
public init(presentationData: ItemListPresentationData, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
self.presentationData = presentationData
self.style = style
self.sectionId = sectionId
self.sortOrder = sortOrder
self.displayOrder = displayOrder
self.context = context
@ -208,7 +212,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader {
let node = ContactsPeerItemNode()
let makeLayout = node.asyncLayout()
let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader)
let (nodeLayout, nodeApply) = makeLayout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
@ -229,7 +233,7 @@ public class ContactsPeerItem: ListViewItem, ListViewItemWithHeader {
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader)
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply().1(animation.isAnimated, false)
@ -279,6 +283,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0)
public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topSeparatorNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
@ -296,7 +301,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var isHighlighted: Bool = false
private var peerPresenceManager: PeerPresenceStatusManager?
private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool)?
private var layoutParams: (ContactsPeerItem, ListViewItemLayoutParams, Bool, Bool, Bool, ItemListNeighbors)?
public var chatPeer: Peer? {
if let peer = self.layoutParams?.0.peer {
switch peer {
@ -318,6 +323,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topSeparatorNode = ASDisplayNode()
self.topSeparatorNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
@ -337,6 +345,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.isAccessibilityElement = true
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topSeparatorNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
@ -345,7 +354,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4)
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4, layoutParams.5)
let _ = apply()
}
})
@ -360,11 +369,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let (item, _, _, _, _) = self.layoutParams {
if let (item, _, _, _, _, _) = self.layoutParams {
let (first, last, firstWithHeader) = ContactsPeerItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem)
self.layoutParams = (item, params, first, last, firstWithHeader)
self.layoutParams = (item, params, first, last, firstWithHeader, itemListNeighbors(item: item, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader)
let (nodeLayout, nodeApply) = makeLayout(item, params, first, last, firstWithHeader, itemListNeighbors(item: item, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
@ -419,7 +428,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
public func asyncLayout() -> (_ item: ContactsPeerItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeStatusLayout = TextNode.asyncLayout(self.statusNode)
let currentSelectionNode = self.selectionNode
@ -428,7 +437,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let currentItem = self.layoutParams?.0
return { [weak self] item, params, first, last, firstWithHeader in
return { [weak self] item, params, first, last, firstWithHeader, neighbors in
var updatedTheme: PresentationTheme?
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
@ -660,7 +669,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
if let strongSelf = self {
return (.complete(), { [weak strongSelf] animated, synchronousLoads in
if let strongSelf = strongSelf {
strongSelf.layoutParams = (item, params, first, last, firstWithHeader)
strongSelf.layoutParams = (item, params, first, last, firstWithHeader, neighbors)
strongSelf.accessibilityLabel = titleAttributedString?.string
strongSelf.accessibilityValue = statusAttributedString?.string
@ -703,11 +712,31 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let revealOffset = strongSelf.revealOffset
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
switch item.style {
case .plain:
strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
case .blocks:
strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
}
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
switch item.style {
case .plain:
strongSelf.topSeparatorNode.isHidden = true
case .blocks:
switch neighbors.top {
case .sameSection(false):
strongSelf.topSeparatorNode.isHidden = true
default:
strongSelf.topSeparatorNode.isHidden = false
}
}
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
let _ = titleApply()
@ -824,6 +853,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(nodeLayout.insets.top, separatorHeight)), size: CGSize(width: nodeLayout.contentSize.width, height: separatorHeight))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight))
strongSelf.separatorNode.isHidden = last
@ -955,7 +985,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
override public func header() -> ListViewItemHeader? {
if let (item, _, _, _, _) = self.layoutParams {
if let (item, _, _, _, _, _) = self.layoutParams {
return item.header
} else {
return nil

View File

@ -115,6 +115,8 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
private var validLayout: ContainerViewLayout?
public var additionalInsets: UIEdgeInsets = UIEdgeInsets()
private var didPlayPresentationAnimation = false
public private(set) var didAppearOnce = false
public var didAppear: ((Bool) -> Void)?
@ -459,7 +461,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
self.validLayout = layout
(self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition)
(self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, transition: transition, additionalInsets: self.additionalInsets)
}
@objc func leftNavigationButtonPressed() {

View File

@ -369,9 +369,10 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
})
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) {
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.bottom = max(insets.bottom, additionalInsets.bottom)
var addedInsets: UIEdgeInsets?
if layout.size.width > 480.0 {
@ -551,7 +552,7 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
if let validLayout = self.validLayout {
updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
self.insertSubnode(updatedNode, belowSubnode: self.navigationBar)
self.insertSubnode(updatedNode, aboveSubnode: self.listNode)
updatedNode.activate()
}
} else {
@ -684,6 +685,11 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchNode = self.searchNode {
if !self.navigationBar.isHidden && self.navigationBar.supernode != nil {
if let result = self.navigationBar.hitTest(self.view.convert(point, to: self.navigationBar.view), with: event) {
return result
}
}
if let result = searchNode.hitTest(point, with: event) {
return result
}

View File

@ -99,7 +99,7 @@ public class ItemListActivityTextItemNode: ListViewItemNode {
titleString.addAttributes([NSAttributedString.Key.font: titleFont], range: NSMakeRange(0, titleString.length))
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(topLeft: CGSize(width: activityWidth, height: 4.0)), insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - 22.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: TextNodeCutout(topLeft: CGSize(width: activityWidth, height: 22.0)), insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets

View File

@ -62,6 +62,8 @@ static_library(
"//submodules/Markdown:Markdown",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -0,0 +1,456 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import ContactsPeerItem
import SearchUI
import SolidRoundedButtonNode
func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings) -> String {
let timestamp = peer.lastActivityDate
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var t: time_t = time_t(TimeInterval(timestamp))
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(nowTimestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
var string: String
if timeinfoNow.tm_year == timeinfo.tm_year && timeinfoNow.tm_mon == timeinfo.tm_mon {
//weeks
let dif = Int(roundf(Float(timeinfoNow.tm_mday - timeinfo.tm_mday) / 7))
string = strings.OldChannels_InactiveWeek(Int32(dif))
} else if timeinfoNow.tm_year == timeinfo.tm_year {
//month
let dif = Int(timeinfoNow.tm_mon - timeinfo.tm_mon)
string = strings.OldChannels_InactiveMonth(Int32(dif))
} else {
//year
var dif = Int(timeinfoNow.tm_year - timeinfo.tm_year)
if Int(timeinfoNow.tm_mon - timeinfo.tm_mon) > 6 {
dif += 1
}
string = strings.OldChannels_InactiveYear(Int32(dif))
}
if let channel = peer.peer as? TelegramChannel, case .group = channel.info {
if let participantsCount = peer.participantsCount, participantsCount != 0 {
string = strings.OldChannels_GroupFormat(participantsCount) + string
} else {
string = strings.OldChannels_GroupEmptyFormat + string
}
} else {
string = strings.OldChannels_ChannelFormat + string
}
return string
}
private final class OldChannelsItemArguments {
let context: AccountContext
let togglePeer: (PeerId) -> Void
init(
context: AccountContext,
togglePeer: @escaping (PeerId) -> Void
) {
self.context = context
self.togglePeer = togglePeer
}
}
private enum OldChannelsSection: Int32 {
case info
case peers
}
private enum OldChannelsEntryId: Hashable {
case info
case peersHeader
case peer(PeerId)
}
private enum OldChannelsEntry: ItemListNodeEntry {
case info(String, String)
case peersHeader(String)
case peer(Int, InactiveChannel, Bool)
var section: ItemListSectionId {
switch self {
case .info:
return OldChannelsSection.info.rawValue
case .peersHeader, .peer:
return OldChannelsSection.peers.rawValue
}
}
var stableId: OldChannelsEntryId {
switch self {
case .info:
return .info
case .peersHeader:
return .peersHeader
case let .peer(_, peer, _):
return .peer(peer.peer.id)
}
}
static func ==(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool {
switch lhs {
case let .info(title, text):
if case .info(title, text) = rhs {
return true
} else {
return false
}
case let .peersHeader(title):
if case .peersHeader(title) = rhs {
return true
} else {
return false
}
case let .peer(lhsIndex, lhsPeer, lhsSelected):
if case let .peer(rhsIndex, rhsPeer, rhsSelected) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsSelected != rhsSelected {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool {
switch lhs {
case .info:
if case .info = rhs {
return false
} else {
return true
}
case .peersHeader:
switch rhs {
case .info, .peersHeader:
return false
case .peer:
return true
}
case let .peer(lhsIndex, _, _):
switch rhs {
case .info, .peersHeader:
return false
case let .peer(rhsIndex, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! OldChannelsItemArguments
switch self {
case let .info(title, text):
return ItemListInfoItem(presentationData: presentationData, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: nil)
case let .peersHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
arguments.togglePeer(peer.peer.id)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}
}
}
private struct OldChannelsState: Equatable {
var selectedPeers: Set<PeerId> = Set()
var isSearching: Bool = false
}
private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, peers: [InactiveChannel]?) -> [OldChannelsEntry] {
var entries: [OldChannelsEntry] = []
if let peers = peers, !peers.isEmpty {
entries.append(.info(presentationData.strings.OldChannels_NoticeTitle, presentationData.strings.OldChannels_NoticeText))
entries.append(.peersHeader(presentationData.strings.OldChannels_ChannelsHeader))
for peer in peers {
entries.append(.peer(entries.count, peer, state.selectedPeers.contains(peer.peer.id)))
}
}
return entries
}
private final class OldChannelsActionPanelNode: ASDisplayNode {
private let separatorNode: ASDisplayNode
private let buttonNode: SolidRoundedButtonNode
init(presentationData: ItemListPresentationData, leaveAction: @escaping () -> Void) {
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.buttonNode = SolidRoundedButtonNode(title: presentationData.strings.OldChannels_Leave, icon: nil, theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = {
leaveAction()
}
}
func updatePresentationData(_ presentationData: ItemListPresentationData) {
self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
}
func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
let sideInset: CGFloat = 16.0
let verticalInset: CGFloat = 16.0
let buttonHeight: CGFloat = 50.0
let insets = layout.insets(options: [.input])
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
self.buttonNode.updateLayout(width: layout.size.width - sideInset * 2.0, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: CGSize(width: layout.size.width, height: buttonHeight)))
return buttonHeight + verticalInset * 2.0 + insets.bottom
}
}
private final class OldChannelsControllerImpl: ItemListController {
private let panelNode: OldChannelsActionPanelNode
private var displayPanel: Bool = false
private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
var leaveAction: (() -> Void)?
override init<ItemGenerationArguments>(presentationData: ItemListPresentationData, updatedPresentationData: Signal<ItemListPresentationData, NoError>, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal<ItemListControllerTabBarItem, NoError>?) {
var leaveActionImpl: (() -> Void)?
self.panelNode = OldChannelsActionPanelNode(presentationData: presentationData, leaveAction: {
leaveActionImpl?()
})
super.init(presentationData: presentationData, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem)
self.presentationDataDisposable = (updatedPresentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self else {
return
}
strongSelf.panelNode.updatePresentationData(presentationData)
})
leaveActionImpl = { [weak self] in
self?.leaveAction?()
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override var navigationBarRequiresEntireLayoutUpdate: Bool {
return false
}
override func loadDisplayNode() {
super.loadDisplayNode()
self.displayNode.addSubnode(self.panelNode)
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
let panelHeight = self.panelNode.updateLayout(layout, transition: transition)
var additionalInsets = UIEdgeInsets()
additionalInsets.bottom = max(layout.intrinsicInsets.bottom, panelHeight)
self.additionalInsets = additionalInsets
super.containerLayoutUpdated(layout, transition: transition)
transition.updateFrame(node: self.panelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.displayPanel ? (layout.size.height - panelHeight) : layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)), beginWithCurrentState: true)
}
func updateDisplayPanel(_ value: Bool) {
if self.displayPanel != value {
self.displayPanel = value
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring))
}
}
}
}
public func oldChannelsController(context: AccountContext) -> ViewController {
let initialState = OldChannelsState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((OldChannelsState) -> OldChannelsState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var updateHasSelectedPeersImpl: ((Bool) -> Void)?
var dismissImpl: (() -> Void)?
var setDisplayNavigationBarImpl: ((Bool) -> Void)?
let actionsDisposable = DisposableSet()
let arguments = OldChannelsItemArguments(
context: context,
togglePeer: { peerId in
var hasSelectedPeers = false
updateState { state in
var state = state
if state.selectedPeers.contains(peerId) {
state.selectedPeers.remove(peerId)
} else {
state.selectedPeers.insert(peerId)
}
hasSelectedPeers = !state.selectedPeers.isEmpty
return state
}
updateHasSelectedPeersImpl?(hasSelectedPeers)
}
)
let selectedPeerIds = statePromise.get()
|> map { $0.selectedPeers }
|> distinctUntilChanged
let peersSignal: Signal<[InactiveChannel]?, NoError> = .single(nil)
|> then(
inactiveChannelList(network: context.account.network)
|> map { peers -> [InactiveChannel]? in
return peers.sorted(by: { lhs, rhs in
return lhs.lastActivityDate < rhs.lastActivityDate
})
}
)
let peersPromise = Promise<[InactiveChannel]?>()
peersPromise.set(peersSignal)
var previousPeersWereEmpty = true
let signal = combineLatest(
queue: Queue.mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
peersPromise.get()
)
|> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.OldChannels_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
var searchItem: OldChannelsSearchItem?
searchItem = OldChannelsSearchItem(context: context, theme: presentationData.theme, placeholder: presentationData.strings.Common_Search, activated: state.isSearching, updateActivated: { value in
if !value {
setDisplayNavigationBarImpl?(true)
}
updateState { state in
var state = state
state.isSearching = value
return state
}
if value {
setDisplayNavigationBarImpl?(false)
}
}, peers: peersPromise.get() |> map { $0 ?? [] }, selectedPeerIds: selectedPeerIds, togglePeer: { peerId in
arguments.togglePeer(peerId)
})
let peersAreEmpty = peers == nil
let peersAreEmptyUpdated = previousPeersWereEmpty != peersAreEmpty
previousPeersWereEmpty = peersAreEmpty
var emptyStateItem: ItemListControllerEmptyStateItem?
if peersAreEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = OldChannelsControllerImpl(context: context, state: signal)
controller.navigationPresentation = .modal
updateHasSelectedPeersImpl = { [weak controller] value in
controller?.updateDisplayPanel(value)
}
controller.leaveAction = {
let state = stateValue.with { $0 }
let _ = (peersPromise.get()
|> take(1)
|> mapToSignal { peers in
return context.account.postbox.transaction { transaction -> Void in
if let peers = peers {
for peer in peers {
if transaction.getPeer(peer.peer.id) == nil {
updatePeers(transaction: transaction, peers: [peer.peer], update: { _, updated in
return updated
})
}
removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peer.peer.id, reportChatSpam: false, deleteGloballyIfPossible: false)
}
}
}
}
|> deliverOnMainQueue).start()
dismissImpl?()
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
setDisplayNavigationBarImpl = { [weak controller] display in
controller?.setDisplayNavigationBar(display, transition: .animated(duration: 0.5, curve: .spring))
}
return controller
}

View File

@ -0,0 +1,418 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
import SearchBarNode
import SearchUI
import ChatListSearchItemHeader
import ContactsPeerItem
extension NavigationBarSearchContentNode: ItemListControllerSearchNavigationContentNode {
public func activate() {
}
public func deactivate() {
}
public func setQueryUpdated(_ f: @escaping (String) -> Void) {
}
}
final class OldChannelsSearchItem: ItemListControllerSearch {
let context: AccountContext
let theme: PresentationTheme
let placeholder: String
let activated: Bool
let updateActivated: (Bool) -> Void
let peers: Signal<[InactiveChannel], NoError>
let selectedPeerIds: Signal<Set<PeerId>, NoError>
let togglePeer: (PeerId) -> Void
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<PeerId>, NoError>, togglePeer: @escaping (PeerId) -> Void) {
self.context = context
self.theme = theme
self.placeholder = placeholder
self.activated = activated
self.updateActivated = updateActivated
self.peers = peers
self.selectedPeerIds = selectedPeerIds
self.togglePeer = togglePeer
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? OldChannelsSearchItem {
if self.context !== to.context || self.theme !== to.theme || self.placeholder != to.placeholder || self.activated != to.activated {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> NavigationBarContentNode & ItemListControllerSearchNavigationContentNode {
let updateActivated: (Bool) -> Void = self.updateActivated
if let current = current as? NavigationBarSearchContentNode {
current.updateThemeAndPlaceholder(theme: self.theme, placeholder: self.placeholder)
return current
} else {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
return NavigationBarSearchContentNode(theme: presentationData.theme, placeholder: presentationData.strings.Settings_Search, activate: {
updateActivated(true)
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
let updateActivated: (Bool) -> Void = self.updateActivated
if let current = current as? OldChannelsSearchItemNode, let titleContentNode = titleContentNode as? NavigationBarSearchContentNode {
current.updatePresentationData(self.context.sharedContext.currentPresentationData.with { $0 })
if current.isSearching != self.activated {
if self.activated {
current.activateSearch(placeholderNode: titleContentNode.placeholderNode)
} else {
current.deactivateSearch(placeholderNode: titleContentNode.placeholderNode)
}
}
return current
} else {
return OldChannelsSearchItemNode(context: self.context, cancel: {
updateActivated(false)
}, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer)
}
}
}
private final class OldChannelsSearchInteraction {
let togglePeer: (PeerId) -> Void
init(togglePeer: @escaping (PeerId) -> Void) {
self.togglePeer = togglePeer
}
}
private enum OldChannelsSearchEntry: Comparable, Identifiable {
case peer(Int, InactiveChannel, Bool)
var stableId: PeerId {
switch self {
case let .peer(_, peer, _):
return peer.peer.id
}
}
private func index() -> Int {
switch self {
case let .peer(index, _, _):
return index
}
}
static func <(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool {
return lhs.index() < rhs.index()
}
static func ==(lhs: OldChannelsSearchEntry, rhs: OldChannelsSearchEntry) -> Bool {
if case let .peer(index, peer, isSelected) = lhs {
if case .peer(index, peer, isSelected) = rhs {
return true
}
}
return false
}
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem {
switch self {
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
interaction.togglePeer(peer.peer.id)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}
}
}
private struct OldChannelsSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData, from fromEntries: [OldChannelsSearchEntry], to toEntries: [OldChannelsSearchEntry], context: AccountContext, interaction: OldChannelsSearchInteraction, isSearching: Bool, forceUpdate: Bool) -> OldChannelsSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return OldChannelsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private final class OldChannelsSearchContainerNode: SearchDisplayControllerContentNode {
private let listNode: ListView
private var enqueuedTransitions: [OldChannelsSearchContainerTransition] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
init(context: AccountContext, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<PeerId>, NoError>, togglePeer: @escaping (PeerId) -> Void) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(self.presentationData)
self.listNode = ListView()
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
super.init()
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.listNode)
let interaction = OldChannelsSearchInteraction(togglePeer: { peerId in
togglePeer(peerId)
})
let queryAndFoundItems: Signal<(String, [OldChannelsSearchEntry])?, NoError> = combineLatest(self.searchQuery.get(), peers, selectedPeerIds)
|> mapToSignal { query, peers, selectedPeerIds -> Signal<(String, [OldChannelsSearchEntry])?, NoError> in
if let query = query, !query.isEmpty {
var results: [OldChannelsSearchEntry] = []
let normalizedQuery = query.lowercased()
for peer in peers {
if peer.peer.indexName.matchesByTokens(normalizedQuery) {
results.append(.peer(results.count, peer, selectedPeerIds.contains(peer.peer.id)))
}
}
return .single((query, results))
} else {
return .single(nil)
}
}
let previousEntriesHolder = Atomic<([OldChannelsSearchEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.searchDisposable.set(combineLatest(queue: .mainQueue(), queryAndFoundItems, self.presentationDataPromise.get()).start(next: { [weak self] queryAndFoundItems, presentationData in
guard let strongSelf = self else {
return
}
var currentQuery: String?
var entries: [OldChannelsSearchEntry] = []
if let (query, items) = queryAndFoundItems {
currentQuery = query
for item in items {
entries.append(item)
}
}
if !entries.isEmpty || currentQuery == nil {
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedOldChannelsSearchContainerTransition(presentationData: ItemListPresentationData(presentationData), from: previousEntriesAndPresentationData?.0 ?? [], to: entries, context: context, interaction: interaction, isSearching: queryAndFoundItems != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings)
strongSelf.enqueueTransition(transition)
}
}))
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
strongSelf.presentationDataPromise.set(.single(presentationData))
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.recentDisposable?.dispose()
self.presentationDataDisposable?.dispose()
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: OldChannelsSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
})
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override func scrollToTop() {
let listNodeToScroll: ListView = self.listNode
listNodeToScroll.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
}
private final class OldChannelsSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private var presentationData: PresentationData
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var searchDisplayController: SearchDisplayController?
var cancel: () -> Void
private let peers: Signal<[InactiveChannel], NoError>
private let selectedPeerIds: Signal<Set<PeerId>, NoError>
private let togglePeer: (PeerId) -> Void
init(context: AccountContext, cancel: @escaping () -> Void, peers: Signal<[InactiveChannel], NoError>, selectedPeerIds: Signal<Set<PeerId>, NoError>, togglePeer: @escaping (PeerId) -> Void) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.cancel = cancel
self.peers = peers
self.selectedPeerIds = selectedPeerIds
self.togglePeer = togglePeer
super.init()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.searchDisplayController?.updatePresentationData(presentationData)
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: OldChannelsSearchContainerNode(context: self.context, peers: self.peers, selectedPeerIds: self.selectedPeerIds, togglePeer: self.togglePeer), cancel: { [weak self] in
self?.cancel()
})
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
if isSearchBar {
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
} else {
strongSelf.addSubnode(subnode)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
var isSearching: Bool {
return self.searchDisplayController != nil
}
override func scrollToTop() {
self.searchDisplayController?.contentNode.scrollToTop()
}
override func queryUpdated(_ query: String) {
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchDisplayController = self.searchDisplayController, let result = searchDisplayController.contentNode.hitTest(self.view.convert(point, to: searchDisplayController.contentNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}

View File

@ -353,11 +353,13 @@ private func stringForSelectiveSettings(strings: PresentationStrings, settings:
}
}
private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeSessionsCount: Int, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> [PrivacyAndSecurityEntry] {
private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, accessChallengeData: PostboxAccessChallengeData, blockedPeerCount: Int?, activeWebsitesCount: Int, twoStepAuthData: TwoStepVerificationAccessConfiguration?) -> [PrivacyAndSecurityEntry] {
var entries: [PrivacyAndSecurityEntry] = []
entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers, blockedPeerCount == nil ? "" : (blockedPeerCount == 0 ? presentationData.strings.PrivacySettings_BlockedPeersEmpty : "\(blockedPeerCount!)")))
entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_AuthSessions, activeSessionsCount == 0 ? "" : "\(activeSessionsCount)"))
if activeWebsitesCount != 0 {
entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_WebSessions, activeWebsitesCount == 0 ? "" : "\(activeWebsitesCount)"))
}
let passcodeValue: String
switch accessChallengeData {
@ -427,7 +429,7 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD
return entries
}
public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil) -> ViewController {
public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil) -> ViewController {
let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PrivacyAndSecurityControllerState())
let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in
@ -449,10 +451,12 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
let privacySettingsPromise = Promise<AccountPrivacySettings?>()
privacySettingsPromise.set(.single(initialSettings) |> then(requestAccountPrivacySettings(account: context.account) |> map(Optional.init)))
let blockedPeersContext = BlockedPeersContext(account: context.account)
let blockedPeersContext = blockedPeersContext ?? BlockedPeersContext(account: context.account)
let activeSessionsContext = activeSessionsContext ?? ActiveSessionsContext(account: context.account)
let webSessionsContext = webSessionsContext ?? WebSessionsContext(account: context.account)
webSessionsContext.loadMore()
let updateTwoStepAuthDisposable = MetaDisposable()
actionsDisposable.add(updateTwoStepAuthDisposable)
@ -670,7 +674,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
pushControllerImpl?(controller, true)
}
}, openActiveSessions: {
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext), true)
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: true), true)
}, setupAccountAutoremove: {
let signal = privacySettingsPromise.get()
|> take(1)
@ -740,8 +744,8 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
updatedSettings?(settings)
}))
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), privacySettingsPromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), recentPeers(account: context.account), blockedPeersContext.state, activeSessionsContext.state, context.sharedContext.accountManager.accessChallengeData(), twoStepAuthDataValue.get())
|> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeSessionsState, accessChallengeData, twoStepAuthData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), privacySettingsPromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), recentPeers(account: context.account), blockedPeersContext.state, webSessionsContext.state, context.sharedContext.accountManager.accessChallengeData(), twoStepAuthDataValue.get())
|> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuthData -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if privacySettings == nil || state.updatingAccountTimeoutValue != nil {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
@ -749,7 +753,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PrivacySettings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeSessionsCount: activeSessionsState.sessions.count, twoStepAuthData: twoStepAuthData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, twoStepAuthData: twoStepAuthData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}

View File

@ -244,7 +244,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))

View File

@ -447,7 +447,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
return entries
}
public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext) -> ViewController {
public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool) -> ViewController {
let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: RecentSessionsControllerState())
let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in
@ -459,6 +459,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
@ -468,8 +469,23 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
let terminateOtherSessionsDisposable = MetaDisposable()
actionsDisposable.add(terminateOtherSessionsDisposable)
let mode = ValuePromise<RecentSessionsMode>(.sessions)
let websitesPromise = Promise<([WebAuthorization], [PeerId : Peer])?>(nil)
let didAppearValue = ValuePromise<Bool>(false)
if websitesOnly {
let autoDismissDisposable = (webSessionsContext.state
|> filter { !$0.isLoadingMore && $0.sessions.isEmpty }
|> take(1)
|> mapToSignal { _ in
return didAppearValue.get()
|> filter { $0 }
|> take(1)
}
|> deliverOnMainQueue).start(next: { _ in
dismissImpl?()
})
}
let mode = ValuePromise<RecentSessionsMode>(websitesOnly ? .websites : .sessions)
let arguments = RecentSessionsControllerArguments(context: context, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in
updateState { state in
@ -636,16 +652,11 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
}
let emptyStateItem: ItemListControllerEmptyStateItem? = nil
/*if sessionsState.sessions.isEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
} else if sessionsState.sessions.count == 1 && mode == .sessions {
emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
}*/
let title: ItemListControllerTitle
let entries: [RecentSessionsEntry]
if !websites.isEmpty {
title = .sectionControl([presentationData.strings.AuthSessions_Sessions, presentationData.strings.AuthSessions_LoggedIn], mode.rawValue)
if websitesOnly {
title = .text(presentationData.strings.AuthSessions_LoggedIn)
} else {
title = .text(presentationData.strings.AuthSessions_DevicesTitle)
}
@ -678,6 +689,9 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
controller.titleControlValueChanged = { [weak mode] index in
mode?.set(index == 0 ? .sessions : .websites)
}
controller.didAppear = { _ in
didAppearValue.set(true)
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
@ -686,6 +700,9 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

View File

@ -545,7 +545,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil)))
}),
SettingsSearchableItem(id: .privacy(9), title: strings.PrivacySettings_AuthSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
present(.push, recentSessionsController(context: context, activeSessionsContext: ActiveSessionsContext(account: context.account), webSessionsContext: WebSessionsContext(account: context.account)))
present(.push, recentSessionsController(context: context, activeSessionsContext: ActiveSessionsContext(account: context.account), webSessionsContext: WebSessionsContext(account: context.account), websitesOnly: true))
}),
SettingsSearchableItem(id: .privacy(10), title: strings.PrivacySettings_DeleteAccountTitle, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
presentPrivacySettings(context, present, .accountTimeout)

View File

@ -670,10 +670,8 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
entries.append(.savedMessages(presentationData.theme, PresentationResourcesSettings.savedMessages, presentationData.strings.Settings_SavedMessages))
entries.append(.recentCalls(presentationData.theme, PresentationResourcesSettings.recentCalls, presentationData.strings.CallSettings_RecentCalls))
if enableQRLogin {
if enableQRLogin || otherSessionCount != 0 {
entries.append(.devices(presentationData.theme, UIImage(bundleImageName: "Settings/MenuIcons/Sessions")?.precomposed(), presentationData.strings.Settings_Devices, otherSessionCount == 0 ? presentationData.strings.Settings_AddDevice : "\(otherSessionCount + 1)"))
} else {
entries.append(.stickers(presentationData.theme, PresentationResourcesSettings.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks))
}
let notificationsWarning = shouldDisplayNotificationsPermissionWarning(status: notificationsAuthorizationStatus, suppressed: notificationsWarningSuppressed)
@ -683,9 +681,7 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
entries.append(.themes(presentationData.theme, PresentationResourcesSettings.appearance, presentationData.strings.Settings_Appearance))
let languageName = presentationData.strings.primaryComponent.localizedName
entries.append(.language(presentationData.theme, PresentationResourcesSettings.language, presentationData.strings.Settings_AppLanguage, languageName.isEmpty ? presentationData.strings.Localization_LanguageName : languageName))
if enableQRLogin {
entries.append(.contentStickers(presentationData.theme, PresentationResourcesSettings.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks))
}
entries.append(.contentStickers(presentationData.theme, PresentationResourcesSettings.stickers, presentationData.strings.ChatSettings_Stickers, unreadTrendingStickerPacks == 0 ? "" : "\(unreadTrendingStickerPacks)", archivedPacks))
if hasWallet {
entries.append(.wallet(presentationData.theme, PresentationResourcesSettings.wallet, "Gram Wallet", ""))
@ -1113,7 +1109,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
if count == 0 {
pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext))
} else {
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false))
}
})
})
@ -1267,6 +1263,14 @@ public func settingsController(context: AccountContext, accountManager: AccountM
}
updatePassport()
let updateActiveSessions: () -> Void = {
let _ = (activeSessionsContextAndCount.get()
|> deliverOnMainQueue
|> take(1)).start(next: { activeSessionsContext, _, _ in
activeSessionsContext.loadMore()
})
}
let notificationsAuthorizationStatus = Promise<AccessType>(.allowed)
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
notificationsAuthorizationStatus.set(
@ -1655,6 +1659,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
controller.didAppear = { _ in
updatePassport()
updateNotifyExceptions()
updateActiveSessions()
}
controller.previewItemWithTag = { tag in
if let tag = tag as? SettingsEntryTag, case let .account(id) = tag {

View File

@ -7,29 +7,43 @@ import TelegramApi
public struct InactiveChannel : Equatable {
public let peer: Peer
public let lastActivityDate: Int32
init(peer: Peer, lastActivityDate: Int32) {
public let participantsCount: Int32?
init(peer: Peer, lastActivityDate: Int32, participantsCount: Int32?) {
self.peer = peer
self.lastActivityDate = lastActivityDate
self.participantsCount = participantsCount
}
public static func ==(lhs: InactiveChannel, rhs: InactiveChannel) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.lastActivityDate == rhs.lastActivityDate
return lhs.peer.isEqual(rhs.peer) && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount
}
}
public func inactiveChannelList(network: Network) -> Signal<[InactiveChannel], NoError> {
return network.request(Api.functions.channels.getInactiveChannels())
|> retryRequest
|> map { result in
switch result {
case let .inactiveChats(dates, chats, users):
let channels = chats.compactMap {
parseTelegramGroupOrChannel(chat: $0)
}
var inactive: [InactiveChannel] = []
for (i, channel) in channels.enumerated() {
inactive.append(InactiveChannel(peer: channel, lastActivityDate: dates[i]))
}
return inactive
|> retryRequest
|> map { result in
switch result {
case let .inactiveChats(dates, chats, users):
let channels = chats.compactMap {
parseTelegramGroupOrChannel(chat: $0)
}
var participantsCounts: [PeerId: Int32] = [:]
for chat in chats {
switch chat {
case let .channel(channel):
if let participantsCountValue = channel.participantsCount {
participantsCounts[chat.peerId] = channel.participantsCount
}
default:
break
}
}
var inactive: [InactiveChannel] = []
for (i, channel) in channels.enumerated() {
inactive.append(InactiveChannel(peer: channel, lastActivityDate: dates[i], participantsCount: participantsCounts[channel.id]))
}
return inactive
}
}
}

View File

@ -15,6 +15,9 @@ public func joinChannel(account: Account, peerId: PeerId) -> Signal<RenderedChan
|> take(1)
|> castError(JoinChannelError.self)
|> mapToSignal { peer -> Signal<RenderedChannelParticipant?, JoinChannelError> in
#if DEBUG
return .fail(.tooMuchJoined)
#endif
if let inputChannel = apiInputChannel(peer) {
return account.network.request(Api.functions.channels.joinChannel(channel: inputChannel))
|> mapError { error -> JoinChannelError in

View File

@ -9,6 +9,7 @@ import SwiftSignalKit
import TelegramPresentationData
import AlertUI
import PresentationDataUtils
import PeerInfoUI
private enum SubscriberAction {
case join
@ -118,40 +119,41 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
}
switch action {
case .join:
self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating()
self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id)
|> afterDisposed { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.activityIndicator.isHidden = true
strongSelf.activityIndicator.stopAnimating()
}
case .join:
self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating()
self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id)
|> afterDisposed { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.activityIndicator.isHidden = true
strongSelf.activityIndicator.stopAnimating()
}
}).start(error: { [weak self] error in
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
return
}
let text: String
switch error {
case .tooMuchJoined:
text = presentationInterfaceState.strings.Join_ChannelsTooMuch
default:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
text = presentationInterfaceState.strings.Channel_ErrorAccessDenied
} else {
text = presentationInterfaceState.strings.Group_ErrorAccessDenied
}
}
strongSelf.interfaceInteraction?.presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationInterfaceState.strings.Common_OK, action: {})]), nil)
}))
case .kicked:
break
case .muteNotifications, .unmuteNotifications:
if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer {
self.actionDisposable.set(togglePeerMuted(account: context.account, peerId: peer.id).start())
}
}).start(error: { [weak self] error in
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
return
}
let text: String
switch error {
case .tooMuchJoined:
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(oldChannelsController(context: context))
return
default:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
text = presentationInterfaceState.strings.Channel_ErrorAccessDenied
} else {
text = presentationInterfaceState.strings.Group_ErrorAccessDenied
}
}
strongSelf.interfaceInteraction?.presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationInterfaceState.strings.Common_OK, action: {})]), nil)
}))
case .kicked:
break
case .muteNotifications, .unmuteNotifications:
if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer {
self.actionDisposable.set(togglePeerMuted(account: context.account, peerId: peer.id).start())
}
}
}

View File

@ -74,6 +74,6 @@ final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
if peer.isDeleted {
overrideImage = .deletedIcon
}
self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad)
self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: 38.0, height: 38.0))
}
}