GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,538 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import ItemListPeerItem
private final class ChannelBlacklistControllerArguments {
let context: AccountContext
let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let addPeer: () -> Void
let removePeer: (EnginePeer.Id) -> Void
let openPeer: (RenderedChannelParticipant) -> Void
init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (EnginePeer.Id) -> Void, openPeer: @escaping (RenderedChannelParticipant) -> Void) {
self.context = context
self.addPeer = addPeer
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
self.openPeer = openPeer
}
}
private enum ChannelBlacklistSection: Int32 {
case add
case banned
}
private enum ChannelBlacklistEntryStableId: Hashable {
case index(Int)
case peer(EnginePeer.Id)
}
private enum ChannelBlacklistEntry: ItemListNodeEntry {
case add(PresentationTheme, String)
case addInfo(PresentationTheme, String)
case bannedHeader(PresentationTheme, String)
case peerItem(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId {
switch self {
case .add, .addInfo:
return ChannelBlacklistSection.add.rawValue
case .bannedHeader:
return ChannelBlacklistSection.banned.rawValue
case .peerItem:
return ChannelBlacklistSection.banned.rawValue
}
}
var stableId: ChannelBlacklistEntryStableId {
switch self {
case .add:
return .index(0)
case .addInfo:
return .index(1)
case .bannedHeader:
return .index(2)
case let .peerItem(_, _, _, _, _, participant, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool {
switch lhs {
case let .add(lhsTheme, lhsText):
if case let .add(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addInfo(lhsTheme, lhsText):
if case let .addInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .bannedHeader(lhsTheme, lhsText):
if case let .bannedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .peerItem(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled):
if case let .peerItem(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameOrder != rhsNameOrder {
return false
}
if lhsIndex != rhsIndex {
return false
}
if lhsParticipant != rhsParticipant {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool {
switch lhs {
case let .peerItem(_, _, _, _, index, _, _, _):
switch rhs {
case let .peerItem(_, _, _, _, rhsIndex, _, _, _):
return index < rhsIndex
default:
return false
}
default:
if case let .index(lhsIndex) = lhs.stableId {
if case let .index(rhsIndex) = rhs.stableId {
return lhsIndex < rhsIndex
} else {
return true
}
} else {
assertionFailure()
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelBlacklistControllerArguments
switch self {
case let .add(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.addPeer()
})
case let .addInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .bannedHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled):
var text: ItemListPeerItemText = .none
switch participant.participant {
case let .member(_, _, _, banInfo, _, _):
if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] {
text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary)
}
default:
break
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(participant)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
}
}
}
private struct ChannelBlacklistControllerState: Equatable {
let referenceTimestamp: Int32
let editing: Bool
let peerIdWithRevealedOptions: EnginePeer.Id?
let removingPeerId: EnginePeer.Id?
let searchingMembers: Bool
init(referenceTimestamp: Int32) {
self.referenceTimestamp = referenceTimestamp
self.editing = false
self.peerIdWithRevealedOptions = nil
self.removingPeerId = nil
self.searchingMembers = false
}
init(referenceTimestamp: Int32, editing: Bool, peerIdWithRevealedOptions: EnginePeer.Id?, removingPeerId: EnginePeer.Id?, searchingMembers: Bool) {
self.referenceTimestamp = referenceTimestamp
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.removingPeerId = removingPeerId
self.searchingMembers = searchingMembers
}
static func ==(lhs: ChannelBlacklistControllerState, rhs: ChannelBlacklistControllerState) -> Bool {
if lhs.referenceTimestamp != rhs.referenceTimestamp {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
return false
}
if lhs.removingPeerId != rhs.removingPeerId {
return false
}
if lhs.searchingMembers != rhs.searchingMembers {
return false
}
return true
}
func withUpdatedSearchingMembers(_ searchingMembers: Bool) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(referenceTimestamp: self.referenceTimestamp, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: searchingMembers)
}
func withUpdatedEditing(_ editing: Bool) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(referenceTimestamp: self.referenceTimestamp, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: self.searchingMembers)
}
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: EnginePeer.Id?) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(referenceTimestamp: self.referenceTimestamp, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: self.searchingMembers)
}
func withUpdatedRemovingPeerId(_ removingPeerId: EnginePeer.Id?) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(referenceTimestamp: self.referenceTimestamp, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId, searchingMembers: self.searchingMembers)
}
}
private func channelBlacklistControllerEntries(presentationData: PresentationData, peer: EnginePeer?, state: ChannelBlacklistControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelBlacklistEntry] {
var entries: [ChannelBlacklistEntry] = []
if case let .channel(channel) = peer, let participants = participants {
entries.append(.add(presentationData.theme, presentationData.strings.GroupRemoved_Remove))
let isGroup: Bool
if case .group = channel.info {
isGroup = true
} else {
isGroup = false
}
entries.append(.addInfo(presentationData.theme, isGroup ? presentationData.strings.GroupRemoved_RemoveInfo : presentationData.strings.ChannelRemoved_RemoveInfo))
var index: Int32 = 0
if !participants.isEmpty {
entries.append(.bannedHeader(presentationData.theme, presentationData.strings.GroupRemoved_UsersSectionTitle))
}
for participant in participants {
entries.append(.peerItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index, participant, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id))
index += 1
}
}
return entries
}
public func channelBlacklistController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id) -> ViewController {
let statePromise = ValuePromise(ChannelBlacklistControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970)), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelBlacklistControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970)))
let updateState: ((ChannelBlacklistControllerState) -> ChannelBlacklistControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var getNavigationControllerImpl: (() -> NavigationController?)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissInputImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let updateBannedDisposable = MetaDisposable()
actionsDisposable.add(updateBannedDisposable)
let removePeerDisposable = MetaDisposable()
actionsDisposable.add(removePeerDisposable)
actionsDisposable.add(context.engine.peers.keepPeerUpdated(id: peerId, forceUpdate: false).start())
let blacklistPromise = Promise<[RenderedChannelParticipant]?>(nil)
let arguments = ChannelBlacklistControllerArguments(context: context, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, addPeer: {
var dismissController: (() -> Void)?
let controller = ChannelMembersSearchControllerImpl(params: ChannelMembersSearchControllerParams(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, mode: .ban, openPeer: { peer, participant in
if let participant = participant {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch participant.participant {
case .creator:
return
case let .member(_, _, adminInfo, _, _, _):
if let adminInfo = adminInfo, adminInfo.promotedBy != context.account.peerId {
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
return
}
}
}
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { channel in
guard let _ = channel as? TelegramChannel else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progress = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(progress, nil)
removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))
|> deliverOnMainQueue).start(error: { _ in
}, completed: { [weak progress] in
progress?.dismiss()
dismissController?()
}))
})
}))
dismissController = { [weak controller] in
controller?.dismiss()
}
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, removePeer: { memberId in
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in
}, completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
}, openPeer: { participant in
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { peer in
guard case let .channel(channel) = peer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
if !EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty {
items.append(ActionSheetTextItem(title: EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)))
}
let viewInfoTitle: String
if participant.peer is TelegramChannel {
viewInfoTitle = presentationData.strings.GroupRemoved_ViewChannelInfo
} else {
viewInfoTitle = presentationData.strings.GroupRemoved_ViewUserInfo
}
items.append(ActionSheetButtonItem(title: viewInfoTitle, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if participant.peer is TelegramChannel {
if let navigationController = getNavigationControllerImpl?() {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(participant.peer))))
}
} else if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(infoController)
}
}))
if case .group = channel.info, channel.hasPermission(.inviteMembers) {
items.append(ActionSheetButtonItem(title: presentationData.strings.GroupRemoved_AddToGroup, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let memberId = participant.peer.id
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
let signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil)
|> ignoreValues
|> then(
context.peerChannelMemberCategoriesContextsManager.addMember(engine: context.engine, peerId: peerId, memberId: memberId)
|> map { _ -> Void in
}
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
|> ignoreValues
)
removePeerDisposable.set((signal |> deliverOnMainQueue).start(error: { _ in
}, completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
}))
}
items.append(ActionSheetButtonItem(title: presentationData.strings.GroupRemoved_DeleteUser, color: .destructive, font: .default, enabled: true, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let memberId = participant.peer.id
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: nil) |> deliverOnMainQueue).start(error: { _ in
}, completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
})
})
let (listDisposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { listState in
if case .loading(true) = listState.loadingState, listState.list.isEmpty {
blacklistPromise.set(.single(nil))
} else {
blacklistPromise.set(.single(listState.list))
}
})
actionsDisposable.add(listDisposable)
let previousParticipantsValue = Atomic<[RenderedChannelParticipant]?>(value: nil)
let peer = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), peer, blacklistPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, peer, participants -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
var secondaryRightNavigationButton: ItemListNavigationButton?
if let participants = participants, !participants.isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
}
if !state.editing {
if rightNavigationButton == nil {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedSearchingMembers(true)
}
})
} else {
secondaryRightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedSearchingMembers(true)
}
})
}
}
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if participants == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let previous = previousParticipantsValue.swap(participants)
var searchItem: ItemListControllerSearch?
if state.searchingMembers {
searchItem = ChannelMembersSearchItem(context: context, peerId: peerId, searchContext: nil, searchMode: .searchKicked, cancel: {
updateState { state in
return state.withUpdatedSearchingMembers(false)
}
}, openPeer: { _, rendered in
if let rendered = rendered, case .member = rendered.participant {
arguments.openPeer(rendered)
}
}, pushController: { c in
pushControllerImpl?(c)
}, dismissInput: {
dismissInputImpl?()
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.GroupRemoved_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelBlacklistControllerEntries(presentationData: presentationData, peer: peer, state: state, participants: participants), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && participants != nil && previous!.count >= participants!.count)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
controller.view.endEditing(true)
}
}
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
getNavigationControllerImpl = { [weak controller] in
return controller?.navigationController as? NavigationController
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
controller.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)
}
}
return controller
}
@@ -0,0 +1,112 @@
import Foundation
import UIKit
import AsyncDisplayKit
import UIKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AvatarNode
import AccountContext
final class ChannelDiscussionGroupActionSheetItem: ActionSheetItem {
let context: AccountContext
let channelPeer: Peer
let groupPeer: Peer
let strings: PresentationStrings
let nameDisplayOrder: PresentationPersonNameOrder
init(context: AccountContext, channelPeer: Peer, groupPeer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
self.context = context
self.channelPeer = channelPeer
self.groupPeer = groupPeer
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ChannelDiscussionGroupActionSheetItemNode(theme: theme, context: self.context, channelPeer: self.channelPeer, groupPeer: self.groupPeer, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private let avatarFont = avatarPlaceholderFont(size: 26.0)
private final class ChannelDiscussionGroupActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let channelAvatarNode: AvatarNode
private let channelAvatarOverlay: ASImageNode
private let groupAvatarNode: AvatarNode
private let textNode: ImmediateTextNode
init(theme: ActionSheetControllerTheme, context: AccountContext, channelPeer: Peer, groupPeer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) {
self.theme = theme
self.channelAvatarNode = AvatarNode(font: avatarFont)
self.groupAvatarNode = AvatarNode(font: avatarFont)
self.channelAvatarOverlay = ASImageNode()
self.channelAvatarOverlay.displayWithoutProcessing = true
self.channelAvatarOverlay.displaysAsynchronously = false
self.channelAvatarOverlay.image = generateFilledCircleImage(diameter: 66.0, color: theme.itemBackgroundColor.withAlphaComponent(1.0))
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.textAlignment = .center
super.init(theme: theme)
self.addSubnode(self.groupAvatarNode)
self.addSubnode(self.channelAvatarOverlay)
self.addSubnode(self.channelAvatarNode)
self.addSubnode(self.textNode)
self.channelAvatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: EnginePeer(channelPeer))
self.groupAvatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: EnginePeer(groupPeer))
let text: PresentationStrings.FormattedString
if let channelPeer = channelPeer as? TelegramChannel, let addressName = channelPeer.addressName, !addressName.isEmpty {
text = strings.Channel_DiscussionGroup_PublicChannelLink(EnginePeer(groupPeer).displayTitle(strings: strings, displayOrder: nameDisplayOrder), EnginePeer(channelPeer).displayTitle(strings: strings, displayOrder: nameDisplayOrder))
} else {
text = strings.Channel_DiscussionGroup_PrivateChannelLink(EnginePeer(groupPeer).displayTitle(strings: strings, displayOrder: nameDisplayOrder), EnginePeer(channelPeer).displayTitle(strings: strings, displayOrder: nameDisplayOrder))
}
let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0))
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.string, font: textFont, textColor: theme.primaryTextColor))
for range in text.ranges {
attributedText.addAttribute(.font, value: boldFont, range: range.range)
}
self.textNode.attributedText = attributedText
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let textSize = self.textNode.updateLayout(CGSize(width: constrainedSize.width - 20.0, height: .greatestFiniteMagnitude))
let topInset: CGFloat = 16.0
let avatarSize: CGFloat = 60.0
let textSpacing: CGFloat = 12.0
let bottomInset: CGFloat = 15.0
let avatarOverlap: CGFloat = 10.0
let avatarsWidth = avatarSize * 2.0 - avatarOverlap
let channelAvatarFrame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - avatarsWidth) / 2.0), y: topInset), size: CGSize(width: avatarSize, height: avatarSize))
self.channelAvatarNode.frame = channelAvatarFrame
self.groupAvatarNode.frame = channelAvatarFrame.offsetBy(dx: avatarSize - avatarOverlap, dy: 0.0)
self.channelAvatarOverlay.frame = CGRect(origin: CGPoint(x: channelAvatarFrame.minX - 3.0, y: channelAvatarFrame.minY - 3.0), size: CGSize(width: 66.0, height: 66.0))
self.textNode.frame = CGRect(origin: CGPoint(x: floor((constrainedSize.width - textSize.width) / 2.0), y: topInset + avatarSize + textSpacing), size: textSize)
let size = CGSize(width: constrainedSize.width, height: topInset + avatarSize + textSpacing + textSize.height + bottomInset)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
@@ -0,0 +1,305 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import SearchUI
import ContactsPeerItem
import ItemListUI
private enum ChannelDiscussionGroupSearchContent: Equatable {
case peer(EnginePeer)
static func ==(lhs: ChannelDiscussionGroupSearchContent, rhs: ChannelDiscussionGroupSearchContent) -> Bool {
switch lhs {
case let .peer(lhsPeer):
if case let .peer(rhsPeer) = rhs {
return lhsPeer == rhsPeer
} else {
return false
}
}
}
var peerId: EnginePeer.Id {
switch self {
case let .peer(peer):
return peer.id
}
}
}
private final class ChannelDiscussionGroupSearchInteraction {
let peerSelected: (EnginePeer) -> Void
init(peerSelected: @escaping (EnginePeer) -> Void) {
self.peerSelected = peerSelected
}
}
private struct ChannelDiscussionGroupSearchEntryId: Hashable {
let peerId: EnginePeer.Id
}
private final class ChannelDiscussionGroupSearchEntry: Comparable, Identifiable {
let index: Int
let content: ChannelDiscussionGroupSearchContent
init(index: Int, content: ChannelDiscussionGroupSearchContent) {
self.index = index
self.content = content
}
var stableId: ChannelDiscussionGroupSearchEntryId {
return ChannelDiscussionGroupSearchEntryId(peerId: self.content.peerId)
}
static func ==(lhs: ChannelDiscussionGroupSearchEntry, rhs: ChannelDiscussionGroupSearchEntry) -> Bool {
return lhs.index == rhs.index && lhs.content == rhs.content
}
static func <(lhs: ChannelDiscussionGroupSearchEntry, rhs: ChannelDiscussionGroupSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, interaction: ChannelDiscussionGroupSearchInteraction) -> ListViewItem {
switch self.content {
case let .peer(peer):
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: peer), status: .none, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in
interaction.peerSelected(peer)
})
}
}
}
struct ChannelDiscussionGroupSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func channelDiscussionGroupSearchContainerPreparedRecentTransition(from fromEntries: [ChannelDiscussionGroupSearchEntry], to toEntries: [ChannelDiscussionGroupSearchEntry], isSearching: Bool, context: AccountContext, presentationData: PresentationData, interaction: ChannelDiscussionGroupSearchInteraction) -> ChannelDiscussionGroupSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
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 ChannelDiscussionGroupSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
private struct ChannelDiscussionGroupSearchContainerState: Equatable {
}
final class ChannelDiscussionGroupSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let openPeer: (EnginePeer) -> Void
private let dimNode: ASDisplayNode
private let listNode: ListView
private var enqueuedTransitions: [(ChannelDiscussionGroupSearchContainerTransition, Bool)] = []
private var hasValidLayout = false
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public override var hasDim: Bool {
return true
}
init(context: AccountContext, peers: [EnginePeer], openPeer: @escaping (EnginePeer) -> Void) {
self.context = context
self.openPeer = openPeer
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.dimNode.backgroundColor = .clear
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.isHidden = true
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
/*let statePromise = ValuePromise(ChannelDiscussionGroupSearchContainerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelDiscussionGroupSearchContainerState())
let updateState: ((ChannelDiscussionGroupSearchContainerState) -> ChannelDiscussionGroupSearchContainerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}*/
let interaction = ChannelDiscussionGroupSearchInteraction(peerSelected: { peer in
openPeer(peer)
})
var searchIndex: [EngineDataBuffer: [EnginePeer]] = [:]
for peer in peers {
for token in peer._asPeer().indexName.indexTokens {
if searchIndex[token] == nil {
searchIndex[token] = []
}
searchIndex[token]!.append(peer)
}
}
let foundItems = searchQuery.get()
|> mapToSignal { query -> Signal<[ChannelDiscussionGroupSearchEntry]?, NoError> in
guard let query = query, !query.isEmpty else {
return .single(nil)
}
var entries: [ChannelDiscussionGroupSearchEntry] = []
let searchQueryTokens = context.engine.peers.tokenizeSearchString(string: query.lowercased(), transliteration: .none)
var filteredPeers: [EnginePeer] = []
var existingPeers = Set<EnginePeer.Id>()
for (key, values) in searchIndex {
inner: for token in searchQueryTokens {
if token.isPrefix(to: key) {
for peer in values {
if !existingPeers.contains(peer.id) {
existingPeers.insert(peer.id)
filteredPeers.append(peer)
}
}
break inner
}
}
}
for peer in filteredPeers {
entries.append(ChannelDiscussionGroupSearchEntry(index: entries.count, content: .peer(peer)))
}
return .single(entries)
}
let previousSearchItems = Atomic<[ChannelDiscussionGroupSearchEntry]?>(value: nil)
self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] entries, presentationData in
if let strongSelf = self {
let previousEntries = previousSearchItems.swap(entries)
let firstTime = previousEntries == nil
let transition = channelDiscussionGroupSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, context: context, presentationData: presentationData, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
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)
}
}
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
private 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: ChannelDiscussionGroupSearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
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(.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
self?.dimNode.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.dimNode.frame = CGRect(origin: CGPoint(), size: layout.size)
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 !hasValidLayout {
hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override func scrollToTop() {
self.listNode.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?()
}
}
}
@@ -0,0 +1,688 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import ItemListPeerItem
import ItemListPeerActionItem
import ChatListFilterSettingsHeaderItem
import UndoUI
import OldChannelsController
private final class ChannelDiscussionGroupSetupControllerArguments {
let context: AccountContext
let createGroup: () -> Void
let selectGroup: (EnginePeer.Id) -> Void
let unlinkGroup: () -> Void
init(context: AccountContext, createGroup: @escaping () -> Void, selectGroup: @escaping (EnginePeer.Id) -> Void, unlinkGroup: @escaping () -> Void) {
self.context = context
self.createGroup = createGroup
self.selectGroup = selectGroup
self.unlinkGroup = unlinkGroup
}
}
private enum ChannelDiscussionGroupSetupControllerSection: Int32 {
case header
case groups
case unlink
}
private enum ChannelDiscussionGroupSetupControllerEntryStableId: Hashable {
case id(Int)
case peer(EnginePeer.Id)
}
private enum ChannelDiscussionGroupSetupControllerEntry: ItemListNodeEntry {
case header(PresentationTheme, PresentationStrings, String?, Bool, String)
case create(PresentationTheme, String)
case group(Int, PresentationTheme, PresentationStrings, EnginePeer, PresentationPersonNameOrder)
case groupsInfo(PresentationTheme, String)
case unlink(PresentationTheme, String)
var section: Int32 {
switch self {
case .header:
return ChannelDiscussionGroupSetupControllerSection.header.rawValue
case .create, .group, .groupsInfo:
return ChannelDiscussionGroupSetupControllerSection.groups.rawValue
case .unlink:
return ChannelDiscussionGroupSetupControllerSection.unlink.rawValue
}
}
var stableId: ChannelDiscussionGroupSetupControllerEntryStableId {
switch self {
case .header:
return .id(0)
case .create:
return .id(1)
case let .group(_, _, _, peer, _):
return .peer(peer.id)
case .groupsInfo:
return .id(2)
case .unlink:
return .id(3)
}
}
static func ==(lhs: ChannelDiscussionGroupSetupControllerEntry, rhs: ChannelDiscussionGroupSetupControllerEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsStrings, lhsTitle, lhsIsGroup, lhsLabel):
if case let .header(rhsTheme, rhsStrings, rhsTitle, rhsIsGroup, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsIsGroup == rhsIsGroup, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .create(lhsTheme, lhsTitle):
if case let .create(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .group(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsNameOrder):
if case let .group(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsNameOrder) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings == rhsStrings, lhsPeer == rhsPeer, lhsNameOrder == rhsNameOrder {
return true
} else {
return false
}
case let .groupsInfo(lhsTheme, lhsTitle):
if case let .groupsInfo(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .unlink(lhsTheme, lhsTitle):
if case let .unlink(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
}
}
private var sortIndex: Int {
switch self {
case .header:
return 0
case .create:
return 1
case let .group(index, _, _, _, _):
return 10 + index
case .groupsInfo:
return 1000
case .unlink:
return 1001
}
}
static func <(lhs: ChannelDiscussionGroupSetupControllerEntry, rhs: ChannelDiscussionGroupSetupControllerEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelDiscussionGroupSetupControllerArguments
switch self {
case let .header(_, _, title, isGroup, _):
let text: String
if let title = title {
if isGroup {
text = presentationData.strings.Channel_CommentsGroup_HeaderGroupSet(title).string
} else {
text = presentationData.strings.Channel_CommentsGroup_HeaderSet(title).string
}
} else {
text = presentationData.strings.Channel_CommentsGroup_Header
}
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .discussionGroupSetup, sectionId: self.section)
case let .create(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.createGroup()
})
case let .group(_, _, strings, peer, nameOrder):
let text: String
if case let .channel(peer) = peer, let addressName = peer.addressName, !addressName.isEmpty {
text = "@\(addressName)"
} else if case let .channel(peer) = peer, case .broadcast = peer.info {
text = strings.Channel_DiscussionGroup_PrivateChannel
} else {
text = strings.Channel_DiscussionGroup_PrivateGroup
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: nameOrder, context: arguments.context, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .text(text, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.selectGroup(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .groupsInfo(_, title):
return ItemListTextItem(presentationData: presentationData, text: .plain(title), sectionId: self.section)
case let .unlink(_, title):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.unlinkGroup()
})
}
}
}
private func channelDiscussionGroupSetupControllerEntries(presentationData: PresentationData, peer: EnginePeer?, linkedDiscussionPeer: EnginePeer?, groups: [EnginePeer]?) -> [ChannelDiscussionGroupSetupControllerEntry] {
guard case let .channel(peer) = peer else {
return []
}
let canEditChannel = peer.hasPermission(.changeInfo)
var entries: [ChannelDiscussionGroupSetupControllerEntry] = []
if let group = linkedDiscussionPeer {
if case .group = peer.info {
entries.append(.header(presentationData.theme, presentationData.strings, group.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), true, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
} else {
entries.append(.header(presentationData.theme, presentationData.strings, group.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), false, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
}
entries.append(.group(0, presentationData.theme, presentationData.strings, group, presentationData.nameDisplayOrder))
entries.append(.groupsInfo(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Info))
if canEditChannel {
let unlinkText: String
if case .group = peer.info {
unlinkText = presentationData.strings.Channel_DiscussionGroup_UnlinkChannel
} else {
unlinkText = presentationData.strings.Channel_DiscussionGroup_UnlinkGroup
}
entries.append(.unlink(presentationData.theme, unlinkText))
}
} else if case .broadcast = peer.info, canEditChannel {
if let groups = groups {
entries.append(.header(presentationData.theme, presentationData.strings, nil, true, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
entries.append(.create(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Create))
var index = 0
for group in groups {
entries.append(.group(index, presentationData.theme, presentationData.strings, group, presentationData.nameDisplayOrder))
index += 1
}
entries.append(.groupsInfo(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Info))
}
}
return entries
}
private struct ChannelDiscussionGroupSetupControllerState: Equatable {
var searching: Bool = false
}
public func channelDiscussionGroupSetupController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id) -> ViewController {
let statePromise = ValuePromise(ChannelDiscussionGroupSetupControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelDiscussionGroupSetupControllerState())
let updateState: ((ChannelDiscussionGroupSetupControllerState) -> ChannelDiscussionGroupSetupControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let groupPeers = Promise<[EnginePeer]?>()
groupPeers.set(.single(nil)
|> then(
context.engine.peers.availableGroupsForChannelDiscussion()
|> map(Optional.init)
|> `catch` { _ -> Signal<[EnginePeer]?, NoError> in
return .single(nil)
}
))
struct PeerData {
var peer: EnginePeer?
var linkedDiscussionPeer: EnginePeer?
var hasLinkedDiscussionPeerValue: Bool
}
let peerView: Signal<PeerData, NoError> = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.LinkedDiscussionPeerId(id: peerId)
)
|> mapToSignal { peer, linkedDiscussionPeerId -> Signal<PeerData, NoError> in
var linkedDiscussionPeer: Signal<EnginePeer?, NoError>
var hasLinkedDiscussionPeerValue: Bool = false
if case let .known(linkedDiscussionPeerId) = linkedDiscussionPeerId {
hasLinkedDiscussionPeerValue = true
if let linkedDiscussionPeerIdValue = linkedDiscussionPeerId {
linkedDiscussionPeer = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: linkedDiscussionPeerIdValue)
)
} else {
linkedDiscussionPeer = .single(nil)
}
} else {
linkedDiscussionPeer = .single(nil)
}
return linkedDiscussionPeer
|> map { linkedDiscussionPeer -> PeerData in
return PeerData(
peer: peer,
linkedDiscussionPeer: linkedDiscussionPeer,
hasLinkedDiscussionPeerValue: hasLinkedDiscussionPeerValue
)
}
}
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var navigateToGroupImpl: ((EnginePeer.Id) -> Void)?
let actionsDisposable = DisposableSet()
let applyGroupDisposable = MetaDisposable()
actionsDisposable.add(applyGroupDisposable)
let arguments = ChannelDiscussionGroupSetupControllerArguments(context: context, createGroup: {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
pushControllerImpl?(context.sharedContext.makeCreateGroupController(context: context, peerIds: [], initialTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + " Chat", mode: .supergroup, completion: { groupId, dismiss in
var applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: groupId)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
dismiss()
}, completed: {
dismiss()
}))
}))
})
}, selectGroup: { groupId in
dismissInputImpl?()
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.LinkedDiscussionPeerId(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: groupId)
)
|> deliverOnMainQueue).start(next: { linkedDiscussionPeerId, channelPeer, groupPeer in
guard let channelPeer = channelPeer, let groupPeer = groupPeer else {
return
}
if case let .known(maybeLinkedDiscussionPeerId) = linkedDiscussionPeerId, maybeLinkedDiscussionPeerId == groupId {
navigateToGroupImpl?(groupId)
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if case let .channel(channel) = groupPeer, channel.isForum {
let text = presentationData.strings.PeerInfo_TopicsLimitedDiscussionGroups
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_topics", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
return
}
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ChannelDiscussionGroupActionSheetItem(context: context, channelPeer: channelPeer._asPeer(), groupPeer: groupPeer._asPeer(), strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
ActionSheetButtonItem(title: presentationData.strings.Channel_DiscussionGroup_LinkGroup, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
var applySignal: Signal<Bool, ChannelDiscussionGroupError>
var updatedPeerId: EnginePeer.Id? = nil
if case let .legacyGroup(legacyGroup) = groupPeer {
applySignal = context.engine.peers.convertGroupToSupergroup(peerId: legacyGroup.id)
|> mapError { error -> ChannelDiscussionGroupError in
switch error {
case .tooManyChannels:
return .tooManyChannels
default:
return .generic
}
}
|> deliverOnMainQueue
|> mapToSignal { resultPeerId -> Signal<Bool, ChannelDiscussionGroupError> in
updatedPeerId = resultPeerId
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: resultPeerId))
|> castError(ChannelDiscussionGroupError.self)
|> mapToSignal { groupPeer -> Signal<Bool, ChannelDiscussionGroupError> in
if let groupPeer = groupPeer {
let _ = (groupPeers.get()
|> take(1)
|> deliverOnMainQueue).start(next: { groups in
guard var groups = groups else {
return
}
for i in 0 ..< groups.count {
if groups[i].id == groupId {
groups[i] = groupPeer
break
}
}
groupPeers.set(.single(groups))
})
}
return context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: resultPeerId)
}
}
} else {
applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: groupId)
}
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { error in
switch error {
case .tooManyChannels:
pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade))
case .generic, .hasNotPermissions:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
updateState { state in
var state = state
state.searching = false
return state
}
case .groupHistoryIsCurrentlyPrivate:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublic, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublicProceed, action: {
var applySignal: Signal<Bool, ChannelDiscussionGroupError> = context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: updatedPeerId ?? groupId, historyAvailableForNewMembers: true)
|> mapError { _ -> ChannelDiscussionGroupError in
return .generic
}
|> mapToSignal { _ -> Signal<Bool, ChannelDiscussionGroupError> in
return .complete()
}
|> then(
context.engine.peers.updateGroupDiscussionForChannel(channelId: peerId, groupId: updatedPeerId ?? groupId)
)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
updateState { state in
var state = state
state.searching = false
return state
}
}, completed: {
updateState { state in
var state = state
state.searching = false
return state
}
}))
})]), nil)
}
}, completed: {
updateState { state in
var state = state
state.searching = false
return state
}
}))
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
})
}, unlinkGroup: {
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.LinkedDiscussionPeerId(id: peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { linkedDiscussionPeerId, peer in
guard case let .channel(peer) = peer else {
return
}
let applyPeerId: EnginePeer.Id
if case .broadcast = peer.info {
applyPeerId = peerId
} else if case let .known(maybeLinkedDiscussionPeerId) = linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId {
applyPeerId = linkedDiscussionPeerId
} else {
return
}
var applySignal = context.engine.peers.updateGroupDiscussionForChannel(channelId: applyPeerId, groupId: nil)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}, completed: {
if case .group = peer.info {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .success)
presentControllerImpl?(controller, nil)
dismissImpl?()
}
}))
})
})
var wasEmpty: Bool?
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), peerView, groupPeers.get())
|> deliverOnMainQueue
|> map { presentationData, state, view, groups -> (ItemListControllerState, (ItemListNodeState, Any)) in
let title: String
if case let .channel(peer) = view.peer, case .broadcast = peer.info {
title = presentationData.strings.Channel_DiscussionGroup
} else {
title = presentationData.strings.Group_LinkedChannel
}
var crossfade = false
var isEmptyState = false
var displayGroupList = false
if view.hasLinkedDiscussionPeerValue {
let isEmpty: Bool
isEmpty = view.linkedDiscussionPeer == nil
if case let .channel(peer) = view.peer, case .broadcast = peer.info {
if isEmpty {
if groups == nil {
isEmptyState = true
} else {
displayGroupList = true
}
}
}
} else {
isEmptyState = true
}
if let wasEmpty = wasEmpty, wasEmpty != isEmptyState {
crossfade = true
}
wasEmpty = isEmptyState
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmptyState {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
var rightNavigationButton: ItemListNavigationButton?
var searchItem: ItemListControllerSearch?
if let groups = groups, groups.count >= 10, displayGroupList {
if state.searching {
searchItem = ChannelDiscussionGroupSetupSearchItem(context: context, peers: groups, cancel: {
updateState { state in
var state = state
state.searching = false
return state
}
}, dismissInput: {
dismissInputImpl?()
}, openPeer: { peer in
arguments.selectGroup(peer.id)
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.searching = true
return state
}
})
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelDiscussionGroupSetupControllerEntries(presentationData: presentationData, peer: view.peer, linkedDiscussionPeer: view.linkedDiscussionPeer, groups: groups), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
if let controller = controller {
(controller.navigationController as? NavigationController)?.filterController(controller, animated: true)
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
navigateToGroupImpl = { [weak controller] groupId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: groupId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
guard let navigationController = controller?.navigationController as? NavigationController else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always))
})
}
return controller
}
@@ -0,0 +1,181 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import TextFormat
import AppBundle
import Markdown
class ChannelDiscussionGroupSetupHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let title: String?
let isGroup: Bool
let label: String
let sectionId: ItemListSectionId
let isAlwaysPlain: Bool = true
init(theme: PresentationTheme, strings: PresentationStrings, title: String?, isGroup: Bool, label: String, sectionId: ItemListSectionId) {
self.theme = theme
self.strings = strings
self.title = title
self.isGroup = isGroup
self.label = label
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChannelDiscussionGroupSetupHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? ChannelDiscussionGroupSetupHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let iconFont = Font.medium(12.0)
private let titleFont = Font.regular(14.0)
private let titleBoldFont = Font.semibold(14.0)
class ChannelDiscussionGroupSetupHeaderItemNode: ListViewItemNode {
private let imageNode: ASImageNode
private let titleNode: TextNode
private let labelNode: TextNode
private var item: ChannelDiscussionGroupSetupHeaderItem?
init() {
self.imageNode = ASImageNode()
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false)
self.addSubnode(self.imageNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
}
func asyncLayout() -> (_ item: ChannelDiscussionGroupSetupHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
let currentIconImage = self.imageNode.image
return { item, params, neighbors in
let topInset: CGFloat = 30.0
let bottomInset: CGFloat = 0.0
let iconSpacing: CGFloat = 21.0
let iconImage: UIImage?
if item.theme !== currentItem?.theme {
switch item.theme.name {
case let .builtin(name):
switch name {
case .day, .dayClassic:
iconImage = UIImage(bundleImageName: "Chat/Info/DiscussDayIcon")?.precomposed()
case .nightAccent:
iconImage = UIImage(bundleImageName: "Chat/Info/DiscussNightAccentIcon")?.precomposed()
case .night:
iconImage = UIImage(bundleImageName: "Chat/Info/DiscussNightIcon")?.precomposed()
}
case .custom:
iconImage = UIImage(bundleImageName: "Chat/Info/DiscussDayIcon")?.precomposed()
}
} else {
iconImage = currentIconImage
}
let body = MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.sectionHeaderTextColor)
let bold = MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.sectionHeaderTextColor)
let string: NSAttributedString
if let title = item.title {
string = addAttributesToStringWithRanges(item.isGroup ? item.strings.Channel_CommentsGroup_HeaderGroupSet(title)._tuple : item.strings.Channel_CommentsGroup_HeaderSet(title)._tuple, body: body, argumentAttributes: [0: bold])
} else {
string = NSAttributedString(string: item.strings.Channel_CommentsGroup_Header, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor)
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: iconFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
var insets = UIEdgeInsets()
insets.top = topInset
insets.bottom = bottomInset
contentSize = CGSize(width: params.width, height: (iconImage?.size.height ?? 0.0) + iconSpacing + titleLayout.size.height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
let _ = labelApply()
strongSelf.imageNode.image = iconImage
let iconSize = iconImage?.size ?? CGSize()
let iconFrame = CGRect(origin: CGPoint(x: floor((params.width - iconSize.width) / 2.0), y: 0.0), size: iconSize)
strongSelf.imageNode.frame = iconFrame
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((params.width - labelLayout.size.width) / 2.0), y: iconFrame.minY + 63.0), size: labelLayout.size)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.size.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,320 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import SearchBarNode
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ActivityIndicator
final class ChannelDiscussionGroupSetupSearchItem: ItemListControllerSearch {
let context: AccountContext
let peers: [EnginePeer]
let cancel: () -> Void
let dismissInput: () -> Void
let openPeer: (EnginePeer) -> Void
init(context: AccountContext, peers: [EnginePeer], cancel: @escaping () -> Void, dismissInput: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void) {
self.context = context
self.peers = peers
self.cancel = cancel
self.dismissInput = dismissInput
self.openPeer = openPeer
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? ChannelDiscussionGroupSetupSearchItem {
if self.context !== to.context {
return false
}
if self.peers.count != to.peers.count {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let current = current as? ChannelDiscussionSearchNavigationContentNode {
current.updateTheme(presentationData.theme)
return current
} else {
return ChannelDiscussionSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { _ in
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return ChannelDiscussionGroupSetupSearchItemNode(context: self.context, peers: self.peers, openPeer: self.openPeer, cancel: self.cancel, updateActivity: { _ in
}, dismissInput: self.dismissInput)
}
}
private final class ChannelDiscussionGroupSetupSearchItemNode: ItemListControllerSearchNode {
private let containerNode: ChannelDiscussionGroupSearchContainerNode
init(context: AccountContext, peers: [EnginePeer], openPeer: @escaping (EnginePeer) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping (Bool) -> Void, dismissInput: @escaping () -> Void) {
self.containerNode = ChannelDiscussionGroupSearchContainerNode(context: context, peers: peers, openPeer: { peer in
openPeer(peer)
})
self.containerNode.dismissInput = {
dismissInput()
}
self.containerNode.cancel = {
cancel()
}
super.init()
self.addSubnode(self.containerNode)
}
override func queryUpdated(_ query: String) {
self.containerNode.searchTextUpdated(text: query)
}
override func scrollToTop() {
self.containerNode.scrollToTop()
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height)), navigationBarHeight: navigationBarHeight, transition: transition)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
private let searchBarFont = Font.regular(17.0)
private final class ChannelDiscussionSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode {
private struct Params: Equatable {
let size: CGSize
let leftInset: CGFloat
let rightInset: CGFloat
init(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.size = size
self.leftInset = leftInset
self.rightInset = rightInset
}
}
private var theme: PresentationTheme
private let strings: PresentationStrings
private let cancel: () -> Void
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let iconView: UIImageView
private var activityIndicator: ActivityIndicator?
private let searchBar: SearchBarNode
private let close: (background: GlassBackgroundView, icon: UIImageView)
private var params: Params?
private var queryUpdated: ((String) -> Void)?
var activity: Bool = false {
didSet {
if self.activity != oldValue {
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
}
}
}
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
self.theme = theme
self.strings = strings
self.cancel = cancel
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.iconView = UIImageView()
self.backgroundView.contentView.addSubview(self.iconView)
self.close = (GlassBackgroundView(), UIImageView())
self.close.background.contentView.addSubview(self.close.icon)
self.searchBar = SearchBarNode(
theme: SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: theme.chat.inputPanel.panelControlColor,
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: theme.chat.inputPanel.inputControlColor,
inputClear: theme.chat.inputPanel.panelControlColor,
accent: theme.chat.inputPanel.panelControlAccentColor,
keyboard: theme.rootController.keyboardColor
),
presentationTheme: theme,
strings: strings,
fieldStyle: .inlineNavigation,
forceSeparator: false,
displayBackground: false,
cancelText: nil
)
super.init()
self.view.addSubview(self.backgroundContainer)
self.backgroundView.contentView.addSubview(self.searchBar.view)
self.backgroundContainer.contentView.addSubview(self.close.background)
self.close.background.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.cancel()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
updateActivity({ [weak self] value in
self?.activity = value
})
self.updatePlaceholder()
}
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.searchBar.cancel?()
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
self.updatePlaceholder()
}
func updatePlaceholder() {
let placeholderText: String
placeholderText = self.strings.Channel_DiscussionGroup_SearchPlaceholder
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
override var nominalHeight: CGFloat {
return 60.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
self.params = Params(size: size, leftInset: leftInset, rightInset: rightInset)
let transition = ComponentTransition(transition)
let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 6.0), size: CGSize(width: size.width - 16.0 * 2.0 - leftInset - rightInset - 44.0 - 8.0, height: 44.0))
let closeFrame = CGRect(origin: CGPoint(x: size.width - 16.0 - rightInset - 44.0, y: backgroundFrame.minY), size: CGSize(width: 44.0, height: 44.0))
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundContainer.update(size: size, isDark: self.theme.overallDarkAppearance, transition: transition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
}
transition.setTintColor(view: self.iconView, color: self.theme.rootController.navigationSearchBar.inputIconColor)
if let image = self.iconView.image {
let imageSize: CGSize
let iconFrame: CGRect
let iconFraction: CGFloat = 0.8
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((backgroundFrame.height - imageSize.height) * 0.5)), size: imageSize)
transition.setPosition(view: self.iconView, position: iconFrame.center)
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if self.activity {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.theme.chat.inputPanel.inputControlColor, 14.0, 14.0, false))
self.activityIndicator = activityIndicator
self.backgroundView.contentView.addSubview(activityIndicator.view)
}
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
let indicatorFrame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((backgroundFrame.height - indicatorSize.height) * 0.5)), size: indicatorSize)
transition.setPosition(view: activityIndicator.view, position: indicatorFrame.center)
transition.setBounds(view: activityIndicator.view, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
} else if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil
activityIndicator.view.removeFromSuperview()
}
self.iconView.isHidden = self.activity
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundFrame.width - 36.0 - 4.0, height: 44.0))
transition.setFrame(view: self.searchBar.view, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
if self.close.icon.image == nil {
self.close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 12.0, y: 12.0))
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
context.strokePath()
})?.withRenderingMode(.alwaysTemplate)
}
if let image = close.icon.image {
self.close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
}
self.close.icon.tintColor = self.theme.chat.inputPanel.panelControlColor
transition.setFrame(view: self.close.background, frame: closeFrame)
self.close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
return size
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
@@ -0,0 +1,856 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import ItemListPeerItem
import ItemListPeerActionItem
import InviteLinksUI
import UndoUI
import SendInviteLinkScreen
import Postbox
private final class ChannelMembersControllerArguments {
let context: AccountContext
let addMember: () -> Void
let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let removePeer: (EnginePeer.Id) -> Void
let openParticipant: (RenderedChannelParticipant, Bool) -> Void
let inviteViaLink: () -> Void
let updateHideMembers: (Bool) -> Void
let displayHideMembersTip: (HideMembersDisabledReason) -> Void
init(
context: AccountContext,
addMember: @escaping () -> Void,
setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void,
removePeer: @escaping (EnginePeer.Id) -> Void,
openParticipant: @escaping (RenderedChannelParticipant, Bool) -> Void,
inviteViaLink: @escaping () -> Void,
updateHideMembers: @escaping (Bool) -> Void,
displayHideMembersTip: @escaping (HideMembersDisabledReason) -> Void
) {
self.context = context
self.addMember = addMember
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
self.openParticipant = openParticipant
self.inviteViaLink = inviteViaLink
self.updateHideMembers = updateHideMembers
self.displayHideMembersTip = displayHideMembersTip
}
}
private enum ChannelMembersSection: Int32 {
case hideMembers
case addMembers
case contacts
case peers
}
private enum ChannelMembersEntryStableId: Hashable {
case index(Int32)
case peer(EnginePeer.Id)
}
private enum HideMembersDisabledReason: Equatable {
case notEnoughMembers(Int)
case notAllowed
}
private enum ChannelMembersEntry: ItemListNodeEntry {
case hideMembers(text: String, disabledReason: HideMembersDisabledReason?, isInteractive: Bool, value: Bool)
case hideMembersInfo(String)
case addMember(PresentationTheme, String)
case addMemberInfo(PresentationTheme, String)
case inviteLink(PresentationTheme, String)
case contactsTitle(PresentationTheme, String)
case peersTitle(PresentationTheme, String)
case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, RenderedChannelParticipant, ItemListPeerItemEditing, Bool, Bool, Bool)
var section: ItemListSectionId {
switch self {
case .hideMembers, .hideMembersInfo:
return ChannelMembersSection.hideMembers.rawValue
case .addMember, .addMemberInfo, .inviteLink:
return ChannelMembersSection.addMembers.rawValue
case .contactsTitle:
return ChannelMembersSection.contacts.rawValue
case .peersTitle:
return ChannelMembersSection.peers.rawValue
case let .peerItem(_, _, _, _, _, _, _, _, isContact, _):
return isContact ? ChannelMembersSection.contacts.rawValue : ChannelMembersSection.peers.rawValue
}
}
var stableId: ChannelMembersEntryStableId {
switch self {
case .hideMembers:
return .index(0)
case .hideMembersInfo:
return .index(1)
case .addMember:
return .index(4)
case .addMemberInfo:
return .index(5)
case .inviteLink:
return .index(6)
case .contactsTitle:
return .index(7)
case .peersTitle:
return .index(8)
case let .peerItem(_, _, _, _, _, participant, _, _, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool {
switch lhs {
case let .hideMembers(text, enabled, isInteractive, value):
if case .hideMembers(text, enabled, isInteractive, value) = rhs {
return true
} else {
return false
}
case let .hideMembersInfo(text):
if case .hideMembersInfo(text) = rhs {
return true
} else {
return false
}
case let .addMember(lhsTheme, lhsText):
if case let .addMember(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addMemberInfo(lhsTheme, lhsText):
if case let .addMemberInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .inviteLink(lhsTheme, lhsText):
if case let .inviteLink(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .contactsTitle(lhsTheme, lhsText):
if case let .contactsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .peersTitle(lhsTheme, lhsText):
if case let .peersTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsParticipant, lhsEditing, lhsEnabled, lhsIsContact, lhsIsGroup):
if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsParticipant, rhsEditing, rhsEnabled, rhsIsContact, rhsIsGroup) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsNameOrder != rhsNameOrder {
return false
}
if lhsParticipant != rhsParticipant {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
if lhsIsContact != rhsIsContact {
return false
}
if lhsIsGroup != rhsIsGroup {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool {
switch lhs {
case .hideMembers:
switch rhs {
case .hideMembers:
return false
default:
return true
}
case .hideMembersInfo:
switch rhs {
case .hideMembers, .hideMembersInfo:
return false
default:
return true
}
case .addMember:
switch rhs {
case .hideMembers, .hideMembersInfo, .addMember:
return false
default:
return true
}
case .inviteLink:
switch rhs {
case .hideMembers, .hideMembersInfo, .addMember:
return false
default:
return true
}
case .addMemberInfo:
switch rhs {
case .hideMembers, .hideMembersInfo, .addMember, .inviteLink:
return false
default:
return true
}
case .contactsTitle:
switch rhs {
case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink:
return false
default:
return true
}
case .peersTitle:
switch rhs {
case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink, .contactsTitle:
return false
case let .peerItem(_, _, _, _, _, _, _, _, isContact, _):
return !isContact
default:
return true
}
case let .peerItem(lhsIndex, _, _, _, _, _, _, _, lhsIsContact, _):
switch rhs {
case .contactsTitle:
return false
case .peersTitle:
return lhsIsContact
case let .peerItem(rhsIndex, _, _, _, _, _, _, _, _, _):
return lhsIndex < rhsIndex
case .hideMembers, .hideMembersInfo, .addMember, .addMemberInfo, .inviteLink:
return false
}
}
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelMembersControllerArguments
switch self {
case let .hideMembers(text, disabledReason, isInteractive, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !value && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in
if let disabledReason {
arguments.displayHideMembersTip(disabledReason)
} else {
arguments.updateHideMembers(value)
}
}, activatedWhileDisabled: {
if let disabledReason {
arguments.displayHideMembersTip(disabledReason)
}
})
case let .hideMembersInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .addMember(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.addMember()
})
case let .inviteLink(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.linkIcon(theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.inviteViaLink()
})
case let .addMemberInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .contactsTitle(_, text), let .peersTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled, _, isGroup):
let text: ItemListPeerItemText
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
text = .text(strings.Bot_GenericBotStatus, .secondary)
} else {
text = .presence
}
var labelString: String?
var labelColor: UIColor?
switch participant.participant {
case let .creator(_, _, rank):
labelString = rank ?? strings.Conversation_Owner
labelColor = UIColor(rgb: 0x956ac8)
case let .member(_, _, adminInfo, _, rank, _):
if let _ = adminInfo {
labelString = rank ?? strings.Conversation_Admin
labelColor = UIColor(rgb: 0x49a355)
} else {
labelString = rank
}
}
let label: ItemListPeerItemLabel
if let labelString {
label = .text(labelString, .standard, labelColor ?? presentationData.theme.list.itemSecondaryTextColor, labelColor != nil)
} else {
label = .none
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: participant.presences[participant.peer.id].flatMap(EnginePeer.Presence.init), text: text, label: label, editing: editing, switchValue: nil, enabled: enabled, selectable: participant.peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
arguments.openParticipant(participant, isGroup)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
}
}
}
private struct ChannelMembersControllerState: Equatable {
let editing: Bool
let peerIdWithRevealedOptions: EnginePeer.Id?
let removingPeerId: EnginePeer.Id?
let searchingMembers: Bool
init() {
self.editing = false
self.peerIdWithRevealedOptions = nil
self.removingPeerId = nil
self.searchingMembers = false
}
init(editing: Bool, peerIdWithRevealedOptions: EnginePeer.Id?, removingPeerId: EnginePeer.Id?, searchingMembers: Bool) {
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.removingPeerId = removingPeerId
self.searchingMembers = searchingMembers
}
static func ==(lhs: ChannelMembersControllerState, rhs: ChannelMembersControllerState) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
return false
}
if lhs.removingPeerId != rhs.removingPeerId {
return false
}
if lhs.searchingMembers != rhs.searchingMembers {
return false
}
return true
}
func withUpdatedSearchingMembers(_ searchingMembers: Bool) -> ChannelMembersControllerState {
return ChannelMembersControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: searchingMembers)
}
func withUpdatedEditing(_ editing: Bool) -> ChannelMembersControllerState {
return ChannelMembersControllerState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: self.searchingMembers)
}
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: EnginePeer.Id?) -> ChannelMembersControllerState {
return ChannelMembersControllerState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, searchingMembers: self.searchingMembers)
}
func withUpdatedRemovingPeerId(_ removingPeerId: EnginePeer.Id?) -> ChannelMembersControllerState {
return ChannelMembersControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId, searchingMembers: self.searchingMembers)
}
}
private func channelMembersControllerEntries(context: AccountContext, presentationData: PresentationData, view: PeerView, state: ChannelMembersControllerState, contacts: [RenderedChannelParticipant]?, participants: [RenderedChannelParticipant]?, isGroup: Bool) -> [ChannelMembersEntry] {
if participants == nil || participants?.count == nil {
return []
}
var entries: [ChannelMembersEntry] = []
var displayHideMembers = false
var canSetupHideMembers = false
if let channel = view.peers[view.peerId] as? TelegramChannel, case .group = channel.info {
displayHideMembers = true
canSetupHideMembers = channel.hasPermission(.banMembers)
}
var membersHidden = false
var memberCount: Int?
if let cachedData = view.cachedData as? CachedChannelData, case let .known(value) = cachedData.membersHidden {
membersHidden = value.value
memberCount = cachedData.participantsSummary.memberCount.flatMap(Int.init)
}
if displayHideMembers {
let appConfiguration = context.currentAppConfiguration.with({ $0 })
var minMembers = 100
if let data = appConfiguration.data, let value = data["hidden_members_group_size_min"] as? Double {
minMembers = Int(value)
}
var disabledReason: HideMembersDisabledReason?
if memberCount ?? 0 < minMembers {
disabledReason = .notEnoughMembers(minMembers)
} else if !canSetupHideMembers {
disabledReason = .notAllowed
}
var isInteractive = canSetupHideMembers
if canSetupHideMembers && !membersHidden && disabledReason != nil {
isInteractive = false
}
entries.append(.hideMembers(text: presentationData.strings.GroupMembers_HideMembers, disabledReason: disabledReason, isInteractive: isInteractive, value: membersHidden))
let infoText: String
if membersHidden {
infoText = presentationData.strings.GroupMembers_MembersHiddenOn
} else {
infoText = presentationData.strings.GroupMembers_MembersHiddenOff
}
entries.append(.hideMembersInfo(infoText))
}
if let participants = participants, let contacts = contacts {
var canAddMember: Bool = false
if let peer = view.peers[view.peerId] as? TelegramChannel {
canAddMember = peer.hasPermission(.inviteMembers)
}
var canEditMembers = false
if let peer = view.peers[view.peerId] as? TelegramChannel {
canEditMembers = peer.hasPermission(.banMembers)
}
if canAddMember {
entries.append(.addMember(presentationData.theme, isGroup ? presentationData.strings.Group_Members_AddMembers : presentationData.strings.Channel_Members_AddMembers))
if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil {
entries.append(.inviteLink(presentationData.theme, presentationData.strings.Channel_Members_InviteLink))
}
if let peer = view.peers[view.peerId] as? TelegramChannel {
if peer.flags.contains(.isGigagroup) {
entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Group_Members_AddMembersHelp))
} else if case .broadcast = peer.info {
entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Channel_Members_AddMembersHelp))
}
}
}
var index: Int32 = 0
var existingPeerIds = Set<EnginePeer.Id>()
var addedContactsHeader = false
if !contacts.isEmpty {
addedContactsHeader = true
entries.append(.contactsTitle(presentationData.theme, isGroup ? presentationData.strings.Group_Members_Contacts : presentationData.strings.Channel_Members_Contacts))
for participant in contacts {
var editable = true
if participant.peer.id == context.account.peerId {
editable = false
} else {
switch participant.participant {
case .creator:
editable = false
case .member:
editable = canEditMembers
}
}
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, true, isGroup))
existingPeerIds.insert(participant.peer.id)
index += 1
}
}
var addedOtherHeader = false
for participant in participants {
if existingPeerIds.contains(participant.peer.id) {
continue
}
if addedContactsHeader && !addedOtherHeader {
addedOtherHeader = true
entries.append(.peersTitle(presentationData.theme, isGroup ? presentationData.strings.Group_Members_Other : presentationData.strings.Channel_Members_Other))
}
var editable = true
if participant.peer.id == context.account.peerId {
editable = false
} else {
switch participant.participant {
case .creator:
editable = false
case .member:
editable = canEditMembers
}
}
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id, false, isGroup))
index += 1
}
}
return entries
}
public func channelMembersController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id) -> ViewController {
let statePromise = ValuePromise(ChannelMembersControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelMembersControllerState())
let updateState: ((ChannelMembersControllerState) -> ChannelMembersControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissInputImpl: (() -> Void)?
var getControllerImpl: (() -> ViewController?)?
var displayHideMembersTip: ((HideMembersDisabledReason) -> Void)?
let actionsDisposable = DisposableSet()
let addMembersDisposable = MetaDisposable()
actionsDisposable.add(addMembersDisposable)
let removePeerDisposable = MetaDisposable()
actionsDisposable.add(removePeerDisposable)
let peersPromise = Promise<[RenderedChannelParticipant]?>(nil)
let contactsPromise = Promise<[RenderedChannelParticipant]?>(nil)
let arguments = ChannelMembersControllerArguments(context: context, addMember: {
actionsDisposable.add((combineLatest(
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId)),
peersPromise.get() |> take(1)
)
|> deliverOnMainQueue).start(next: { chatPeer, exportedInvitation, members in
let disabledIds = members?.compactMap({$0.peer.id}) ?? []
let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), filters: [.excludeSelf, .disable(disabledIds)], onlyWriteable: true, isGroupInvitation: true))
addMembersDisposable.set((
contactsController.result
|> deliverOnMainQueue
|> mapToSignal { [weak contactsController] result -> Signal<[(EnginePeer.Id, AddChannelMemberError)], NoError> in
contactsController?.displayProgress = true
var contacts: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
contacts = peerIdsValue
}
let signal = context.peerChannelMemberCategoriesContextsManager.addMembersAllowPartial(engine: context.engine, peerId: peerId, memberIds: contacts.compactMap({ contact -> EnginePeer.Id? in
switch contact {
case let .peer(contactId):
return contactId
default:
return nil
}
}))
return signal
|> deliverOnMainQueue
}).start(next: { [weak contactsController] failedPeerIds in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if failedPeerIds.isEmpty {
contactsController?.dismiss()
} else {
if let chatPeer {
let failedPeers = failedPeerIds.compactMap { _, error -> TelegramForbiddenInvitePeer? in
if case let .restricted(peer) = error {
return peer
} else {
return nil
}
}
if !failedPeers.isEmpty, let contactsController, let navigationController = contactsController.navigationController as? NavigationController {
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 === contactsController }) {
let inviteScreen = SendInviteLinkScreen(context: context, subject: .chat(peer: chatPeer, link: exportedInvitation?.link), peers: failedPeers)
viewControllers.remove(at: index)
viewControllers.append(inviteScreen)
navigationController.setViewControllers(viewControllers, animated: true)
}
} else {
contactsController?.dismiss()
}
return
}
contactsController?.dismiss()
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in
let text: String
switch failedPeerIds[0].1 {
case .limitExceeded:
text = presentationData.strings.Channel_ErrorAddTooMuch
case .tooMuchJoined:
text = presentationData.strings.Invite_ChannelsTooMuch
case .generic:
text = presentationData.strings.Login_UnknownError
case .restricted:
text = presentationData.strings.Channel_ErrorAddBlocked
case .notMutualContact:
if case let .channel(peer) = peer, case .broadcast = peer.info {
text = presentationData.strings.Channel_AddUserLeftError
} else {
text = presentationData.strings.GroupInfo_AddUserLeftError
}
case let .bot(memberId):
guard case let .channel(peer) = peer else {
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
contactsController?.dismiss()
return
}
if peer.hasPermission(.addAdmins) {
contactsController?.displayProgress = false
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_AddBotAsAdmin, action: {
contactsController?.dismiss()
pushControllerImpl?(channelAdminController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in
}, upgradedToSupergroup: { _, f in f () }, transferedOwnership: { _ in }))
})]), nil)
} else {
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_AddBotErrorHaveRights, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
contactsController?.dismiss()
return
case .botDoesntSupportGroups:
text = presentationData.strings.Channel_BotDoesntSupportGroups
case .tooMuchBots:
text = presentationData.strings.Channel_TooMuchBots
case .kicked:
text = presentationData.strings.Channel_AddUserKickedError
}
presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
contactsController?.dismiss()
})
}
}))
presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, removePeer: { memberId in
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))
|> deliverOnMainQueue).start(completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
}, openParticipant: { participant, isGroup in
if isGroup {
if let _ = participant.participant.adminInfo {
let controller = channelAdminController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, adminId: participant.participant.peerId, initialParticipant: participant.participant, updated: { _ in
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
pushControllerImpl?(controller)
} else {
let controller = channelBannedMemberController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, memberId: participant.peer.id, editMember: true, initialParticipant: participant.participant, updated: { rights in
}, upgradedToSupergroup: { _, _ in })
pushControllerImpl?(controller)
}
} else {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(infoController)
}
}
}, inviteViaLink: {
if let controller = getControllerImpl?() {
dismissInputImpl?()
presentControllerImpl?(InviteLinkInviteController(context: context, updatedPresentationData: updatedPresentationData, mode: .groupOrChannel(peerId: peerId), initialInvite: nil, parentNavigationController: controller.navigationController as? NavigationController), nil)
}
}, updateHideMembers: { value in
let _ = context.engine.peers.updateChannelMembersHidden(peerId: peerId, value: value).start()
}, displayHideMembersTip: { disabledReason in
displayHideMembersTip?(disabledReason)
})
let peerView = context.account.viewTracker.peerView(peerId)
let (contactsDisposable, _) = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in
contactsPromise.set(.single(state.list))
})
let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in
peersPromise.set(.single(state.list))
})
actionsDisposable.add(disposable)
actionsDisposable.add(contactsDisposable)
var currentContacts: [RenderedChannelParticipant]?
var currentPeers: [RenderedChannelParticipant]?
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(), presentationData, statePromise.get(), peerView, contactsPromise.get(), peersPromise.get())
|> deliverOnMainQueue
|> map { presentationData, state, view, contacts, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in
var isGroup = true
if let peer = peerViewMainPeer(view) as? TelegramChannel, case .broadcast = peer.info {
isGroup = false
}
var rightNavigationButton: ItemListNavigationButton?
var secondaryRightNavigationButton: ItemListNavigationButton?
var isEmpty = true
if let contacts = contacts, !contacts.isEmpty {
isEmpty = false
} else if let peers = peers, !peers.isEmpty {
isEmpty = false
}
if !isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
if let cachedData = view.cachedData as? CachedChannelData, cachedData.participantsSummary.memberCount ?? 0 >= 200 {
secondaryRightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedSearchingMembers(true)
}
})
}
}
}
var searchItem: ItemListControllerSearch?
if state.searchingMembers {
searchItem = ChannelMembersSearchItem(context: context, peerId: peerId, searchContext: nil, cancel: {
updateState { state in
return state.withUpdatedSearchingMembers(false)
}
}, openPeer: { peer, _ in
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(infoController)
}
}, pushController: { c in
pushControllerImpl?(c)
}, dismissInput: {
dismissInputImpl?()
})
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmpty {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
let previousContacts = currentContacts
currentContacts = contacts
let previousPeers = currentPeers
currentPeers = peers
var animateChanges = false
if let previousContacts = previousContacts, let contacts = contacts, let previousPeers = previousPeers, let peers = peers {
if previousContacts.count >= contacts.count {
animateChanges = true
}
if previousPeers.count >= peers.count {
animateChanges = true
}
}
let title: String = isGroup ? presentationData.strings.Group_Members_Title : presentationData.strings.Channel_Subscribers_Title
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelMembersControllerEntries(context: context, presentationData: presentationData, view: view, state: state, contacts: contacts, participants: peers, isGroup: isGroup), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c)
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
displayHideMembersTip = { [weak controller] reason in
guard let controller else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
switch reason {
case let .notEnoughMembers(minCount):
text = presentationData.strings.PeerInfo_HideMembersLimitedParticipantCountText(Int32(minCount))
case .notAllowed:
text = presentationData.strings.PeerInfo_HideMembersLimitedRights
}
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_topics", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
getControllerImpl = { [weak controller] in
return controller
}
controller.visibleBottomContentOffsetChanged = { offset in
if let loadMoreControl = loadMoreControl, case let .known(value) = offset, value < 40.0 {
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)
}
}
return controller
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import SearchUI
import CounterControllerTitleView
public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMembersSearchController {
private let queue = Queue()
private let context: AccountContext
private let peerId: EnginePeer.Id
private let mode: ChannelMembersSearchControllerMode
private let filters: [ChannelMembersSearchFilter]
private let openPeer: (EnginePeer, RenderedChannelParticipant?) -> Void
public var copyInviteLink: (() -> Void)?
private let forceTheme: PresentationTheme?
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var didPlayPresentationAnimation = false
private var controllerNode: ChannelMembersSearchControllerNode {
return self.displayNode as! ChannelMembersSearchControllerNode
}
private var searchContentNode: NavigationBarSearchContentNode?
public init(params: ChannelMembersSearchControllerParams) {
self.context = params.context
self.peerId = params.peerId
self.mode = params.mode
self.openPeer = params.openPeer
self.filters = params.filters
self.presentationData = params.updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.forceTheme = params.forceTheme
if let forceTheme = params.forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self._hasGlassStyle = true
self.navigationPresentation = .modal
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
let title: String
switch params.mode {
case .ownershipTransfer:
title = self.presentationData.strings.AppointAnotherOwner_Title
let titleView = CounterControllerTitleView(theme: self.presentationData.theme)
titleView.title = CounterControllerTitle(title: title, counter: " ")
self.navigationItem.titleView = titleView
default:
title = self.presentationData.strings.Channel_Members_Title
self.title = title
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateExpansionProgress(1.0, animated: true)
}
strongSelf.controllerNode.scrollToTop()
}
}
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
self?.activateSearch()
})
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
self.presentationDataDisposable = ((params.updatedPresentationData?.signal ?? params.context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self else {
return
}
strongSelf.presentationData = presentationData
strongSelf.controllerNode.updatePresentationData(presentationData)
if let titleView = strongSelf.navigationItem.titleView as? CounterControllerTitleView {
titleView.theme = presentationData.theme
}
})
let _ = (params.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId))
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
switch self.mode {
case .ownershipTransfer:
if let titleView = self.navigationItem.titleView as? CounterControllerTitleView {
titleView.title = CounterControllerTitle(title: titleView.title.title, counter: peer.compactDisplayTitle)
}
default:
if case let .channel(channel) = peer, case .broadcast = channel.info {
self.title = self.presentationData.strings.Channel_Subscribers_Title
} else {
self.title = self.presentationData.strings.Channel_Members_Title
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ChannelMembersSearchControllerNode(context: self.context, presentationData: self.presentationData, forceTheme: self.forceTheme, peerId: self.peerId, mode: self.mode, filters: self.filters)
self.controllerNode.navigationBar = self.navigationBar
self.controllerNode.requestActivateSearch = { [weak self] in
self?.activateSearch()
}
self.controllerNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch(animated: true)
}
self.controllerNode.requestOpenPeerFromSearch = { [weak self] peer, participant in
self?.openPeer(peer, participant)
}
self.controllerNode.requestCopyInviteLink = { [weak self] in
self?.copyInviteLink?()
}
self.controllerNode.pushController = { [weak self] c in
(self?.navigationController as? NavigationController)?.pushViewController(c)
}
self.displayNodeDidLoad()
self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset, _ in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
searchContentNode.updateListVisibleContentOffset(offset)
}
}
self.controllerNode.listNode.didEndScrolling = { [weak self] _ in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode)
}
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
private func activateSearch() {
if self.displayNavigationBar {
if let scrollToTop = self.scrollToTop {
scrollToTop()
}
if let searchContentNode = self.searchContentNode {
self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
}
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
}
}
private func deactivateSearch(animated: Bool) {
if !self.displayNavigationBar {
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
if let searchContentNode = self.searchContentNode {
self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
}
}
}
@objc private func cancelPressed() {
self.dismiss()
}
}
@@ -0,0 +1,757 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import TemporaryCachedPeerDataManager
import SearchBarNode
import ContactsPeerItem
import SearchUI
import ItemListUI
import ContactListUI
import ChatListSearchItemHeader
private final class ChannelMembersSearchInteraction {
let openPeer: (EnginePeer, RenderedChannelParticipant?) -> Void
let copyInviteLink: () -> Void
init(
openPeer: @escaping (EnginePeer, RenderedChannelParticipant?) -> Void,
copyInviteLink: @escaping () -> Void
) {
self.openPeer = openPeer
self.copyInviteLink = copyInviteLink
}
}
private enum ChannelMembersSearchEntryId: Hashable {
case copyInviteLink
case peer(EnginePeer.Id)
case contact(EnginePeer.Id)
}
private enum ChannelMembersSearchEntry: Comparable, Identifiable {
case copyInviteLink
case peer(Int, RenderedChannelParticipant, ContactsPeerItemEditing, String?, Bool, Bool, Bool)
case contact(Int, EnginePeer, TelegramUserPresence?)
var stableId: ChannelMembersSearchEntryId {
switch self {
case .copyInviteLink:
return .copyInviteLink
case let .peer(_, participant, _, _, _, _, _):
return .peer(participant.peer.id)
case let .contact(_, peer, _):
return .contact(peer.id)
}
}
static func ==(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool {
switch lhs {
case .copyInviteLink:
if case .copyInviteLink = rhs {
return true
} else {
return false
}
case let .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled, lhsIsChannel, lhsIsContact):
if case .peer(lhsIndex, lhsParticipant, lhsEditing, lhsLabel, lhsEnabled, lhsIsChannel, lhsIsContact) = rhs {
return true
} else {
return false
}
case let .contact(lhsIndex, lhsPeer, lhsPresence):
if case let .contact(rhsIndex, rhsPeer, rhsPresence) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsPresence != rhsPresence {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChannelMembersSearchEntry, rhs: ChannelMembersSearchEntry) -> Bool {
switch lhs {
case .copyInviteLink:
if case .copyInviteLink = rhs {
return false
} else {
return true
}
case let .peer(lhsIndex, _, _, _, _, _, _):
if case .copyInviteLink = rhs {
return false
} else if case let .peer(rhsIndex, _, _, _, _, _, _) = rhs {
return lhsIndex < rhsIndex
} else if case .contact = rhs {
return true
} else {
return false
}
case let .contact(lhsIndex, _, _):
if case .copyInviteLink = rhs {
return false
} else if case .peer = rhs {
return false
} else if case let .contact(rhsIndex, _, _) = rhs {
return lhsIndex < rhsIndex
} else {
return false
}
}
}
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ListViewItem {
switch self {
case .copyInviteLink:
let icon: ContactListActionItemIcon
if let iconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: presentationData.theme.list.itemAccentColor) {
icon = .generic(iconImage)
} else {
icon = .none
}
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.VoiceChat_CopyInviteLink, icon: icon, clearHighlightAutomatically: true, header: nil, action: {
interaction.copyInviteLink()
})
case let .peer(_, participant, editing, label, enabled, isChannel, isContact):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: NSAttributedString(string: label), multiline: false, isActive: false, icon: nil)
} else if participant.peer.id != context.account.peerId {
let presence = participant.presences[participant.peer.id] ?? TelegramUserPresence(status: .none, lastActivity: 0)
status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat)
} else {
status = .none
}
let headerType: ChatListSearchItemHeaderType
if isContact {
headerType = .contacts
} else {
headerType = isChannel ? .subscribers : .groupMembers
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(participant.peer), chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in
interaction.openPeer(EnginePeer(participant.peer), participant)
})
case let .contact(_, peer, presence):
let status: ContactsPeerItemStatus
if peer.id != context.account.peerId, let presence = presence {
status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat)
} else {
status = .none
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: peer, chatPeer: nil), status: status, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: ChatListSearchItemHeader(type: .contacts, theme: presentationData.theme, strings: presentationData.strings), action: { _ in
interaction.openPeer(peer, nil)
})
}
}
}
private struct ChannelMembersSearchTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let initial: Bool
}
private func preparedTransition(from fromEntries: [ChannelMembersSearchEntry]?, to toEntries: [ChannelMembersSearchEntry], context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: ChannelMembersSearchInteraction) -> ChannelMembersSearchTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries ?? [], rightList: toEntries)
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, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
return ChannelMembersSearchTransition(deletions: deletions, insertions: insertions, updates: updates, initial: fromEntries == nil)
}
class ChannelMembersSearchControllerNode: ASDisplayNode {
private let context: AccountContext
private let peerId: EnginePeer.Id
private let mode: ChannelMembersSearchControllerMode
private let filters: [ChannelMembersSearchFilter]
let listNode: ListView
var navigationBar: NavigationBar?
private var enqueuedTransitions: [ChannelMembersSearchTransition] = []
private(set) var searchDisplayController: SearchDisplayController?
private var containerLayout: (ContainerViewLayout, CGFloat)?
var requestActivateSearch: (() -> Void)?
var requestDeactivateSearch: (() -> Void)?
var requestOpenPeerFromSearch: ((EnginePeer, RenderedChannelParticipant?) -> Void)?
var requestCopyInviteLink: (() -> Void)?
var pushController: ((ViewController) -> Void)?
private let forceTheme: PresentationTheme?
var presentationData: PresentationData
private var disposable: Disposable?
private var listControl: PeerChannelMemberCategoryControl?
init(context: AccountContext, presentationData: PresentationData, forceTheme: PresentationTheme?, peerId: EnginePeer.Id, mode: ChannelMembersSearchControllerMode, filters: [ChannelMembersSearchFilter]) {
self.context = context
self.listNode = ListView()
self.peerId = peerId
self.mode = mode
self.filters = filters
self.presentationData = presentationData
self.forceTheme = forceTheme
if let forceTheme = forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.addSubnode(self.listNode)
let interaction = ChannelMembersSearchInteraction(
openPeer: { [weak self] peer, participant in
self?.requestOpenPeerFromSearch?(peer, participant)
self?.listNode.clearHighlightAnimated(true)
},
copyInviteLink: { [weak self] in
self?.requestCopyInviteLink?()
self?.listNode.clearHighlightAnimated(true)
}
)
let previousEntries = Atomic<[ChannelMembersSearchEntry]?>(value: nil)
let disposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)
let contactsDisposableAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?)?
let additionalDisposable = MetaDisposable()
if peerId.namespace == Namespaces.Peer.CloudGroup {
let disposable = combineLatest(queue: Queue.mainQueue(),
context.account.postbox.peerView(id: peerId),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
)
).start(next: { [weak self] peerView, contactsView in
guard let strongSelf = self else {
return
}
guard let mainPeer = peerViewMainPeer(peerView) else {
return
}
guard let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants else {
return
}
var creatorPeer: EnginePeer?
for participant in participants.participants {
if let peer = peerView.peers[participant.peerId] {
switch participant {
case .creator:
creatorPeer = EnginePeer(peer)
default:
break
}
}
}
guard let creator = creatorPeer else {
return
}
var entries: [ChannelMembersSearchEntry] = []
var canInviteByLink = false
if !(mainPeer.addressName?.isEmpty ?? true) {
canInviteByLink = true
} else if let peer = mainPeer as? TelegramChannel {
if peer.flags.contains(.isCreator) || (peer.adminRights?.rights.contains(.canInviteUsers) == true) {
canInviteByLink = true
}
} else if let peer = mainPeer as? TelegramGroup {
if case .creator = peer.role {
canInviteByLink = true
} else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canInviteUsers) {
canInviteByLink = true
}
}
if case .inviteToCall = mode, canInviteByLink,
!filters.contains(where: { filter in
if case .excludeNonMembers = filter {
return true
} else {
return false
}
}) {
entries.append(.copyInviteLink)
}
var index = 0
for participant in participants.participants {
guard let peer = peerView.peers[participant.peerId] else {
continue
}
if peer.isDeleted {
continue
}
var label: String?
var enabled = true
switch mode {
case .ban:
if peer.id == context.account.peerId {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(peer.id) {
continue
}
case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = peer as? TelegramUser, user.botInfo != nil {
continue
}
}
}
case .promote:
if peer.id == context.account.peerId {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(peer.id) {
continue
}
case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = peer as? TelegramUser, user.botInfo != nil {
continue
}
}
}
if case .creator = participant {
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false
}
case .inviteToCall:
if peer.id == context.account.peerId {
continue
}
if let user = peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(peer.id) {
continue
}
case let .disable(ids):
if ids.contains(peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = peer as? TelegramUser, user.botInfo != nil {
continue
}
}
}
case .ownershipTransfer:
if peer.id == context.account.peerId {
continue
}
if let user = peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) {
continue
}
}
let renderedParticipant: RenderedChannelParticipant
switch participant {
case .creator:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences)
case .admin:
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[creator.id] = creator
peers[peer.id] = EnginePeer(peer)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
case .member:
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[peer.id] = EnginePeer(peer)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
}
entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false))
index += 1
}
if case .inviteToCall = mode, !filters.contains(where: { filter in
if case .excludeNonMembers = filter {
return true
} else {
return false
}
}) {
for peer in contactsView.peers {
entries.append(ChannelMembersSearchEntry.contact(index, peer, contactsView.presences[peer.id]?._asPresence()))
index += 1
}
}
let previous = previousEntries.swap(entries)
strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction))
})
disposableAndLoadMoreControl = (disposable, nil)
contactsDisposableAndLoadMoreControl = nil
} else {
let membersState = Promise<ChannelMemberListState>()
disposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in
membersState.set(.single(state))
})
let contactsState = Promise<ChannelMemberListState>()
contactsDisposableAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.contacts(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, updated: { state in
contactsState.set(.single(state))
})
additionalDisposable.set((combineLatest(queue: .mainQueue(),
membersState.get(),
contactsState.get(),
context.account.postbox.peerView(id: peerId),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
)
).start(next: { [weak self] state, contactsState, peerView, contactsView in
guard let strongSelf = self else {
return
}
var entries: [ChannelMembersSearchEntry] = []
var canInviteByLink = false
var isChannel = false
if let peer = peerViewMainPeer(peerView) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
isChannel = true
}
if !(peer.addressName?.isEmpty ?? true) {
canInviteByLink = true
} else if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) || (peer.adminRights?.rights.contains(.canInviteUsers) == true) {
canInviteByLink = true
}
} else if let peer = peer as? TelegramGroup {
if case .creator = peer.role {
canInviteByLink = true
} else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canInviteUsers) {
canInviteByLink = true
}
}
}
var index = 0
var existingPeersIds = Set<EnginePeer.Id>()
if case .inviteToCall = mode, canInviteByLink, !filters.contains(where: { filter in
if case .excludeNonMembers = filter {
return true
} else {
return false
}
}) {
entries.append(.copyInviteLink)
} else {
contactsLoop: for participant in contactsState.list {
if participant.peer.isDeleted {
continue contactsLoop
}
var label: String?
var enabled = true
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(participant.peer.id) {
continue contactsLoop
}
case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
continue contactsLoop
}
}
}
if case .promote = mode, case .creator = participant.participant {
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false
}
entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, isChannel, true))
index += 1
existingPeersIds.insert(participant.peer.id)
}
}
participantsLoop: for participant in state.list {
if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) {
continue participantsLoop
}
var label: String?
var enabled = true
switch mode {
case .ban, .promote, .ownershipTransfer:
if participant.peer.id == context.account.peerId {
continue participantsLoop
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(participant.peer.id) {
continue participantsLoop
}
case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
continue participantsLoop
}
}
}
if case .promote = mode, case .creator = participant.participant {
label = strongSelf.presentationData.strings.Channel_Management_LabelOwner
enabled = false
}
case .inviteToCall:
if participant.peer.id == context.account.peerId {
continue
}
if let user = participant.peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) {
continue
}
for filter in filters {
switch filter {
case let .exclude(ids):
if ids.contains(participant.peer.id) {
continue participantsLoop
}
case let .disable(ids):
if ids.contains(participant.peer.id) {
enabled = false
}
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
continue participantsLoop
}
}
}
}
entries.append(.peer(index, participant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, isChannel, false))
index += 1
}
if case .inviteToCall = mode, !filters.contains(where: { filter in
if case .excludeNonMembers = filter {
return true
} else {
return false
}
}) {
for peer in contactsView.peers {
entries.append(ChannelMembersSearchEntry.contact(index, peer, contactsView.presences[peer.id]?._asPresence()))
index += 1
}
}
let previous = previousEntries.swap(entries)
strongSelf.enqueueTransition(preparedTransition(from: previous, to: entries, context: context, presentationData: strongSelf.presentationData, nameSortOrder: strongSelf.presentationData.nameSortOrder, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder, interaction: interaction))
})))
}
let combinedDisposable = DisposableSet()
combinedDisposable.add(disposableAndLoadMoreControl.0)
combinedDisposable.add(additionalDisposable)
if let disposable = contactsDisposableAndLoadMoreControl?.0 {
combinedDisposable.add(disposable)
}
self.disposable = combinedDisposable
self.listControl = disposableAndLoadMoreControl.1
if peerId.namespace == Namespaces.Peer.CloudChannel {
self.listNode.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: disposableAndLoadMoreControl.1)
}
}
}
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.view.endEditing(true)
}
}
deinit {
self.disposable?.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
if let forceTheme = forceTheme {
self.presentationData = self.presentationData.withUpdated(theme: forceTheme)
}
self.searchDisplayController?.updatePresentationData(self.presentationData)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.containerLayout != nil
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
var headerInsets = layout.insets(options: [.input])
headerInsets.top += actualNavigationBarHeight
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
return
}
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: self.forceTheme, peerId: self.peerId, mode: .banAndPromoteActions, filters: self.filters, searchContext: nil, openPeer: { [weak self] peer, participant in
self?.requestOpenPeerFromSearch?(peer, participant)
}, updateActivity: { value in
}, pushController: { [weak self] c in
self?.pushController?(c)
}), cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch()
}
}, fieldStyle: placeholderNode.fieldStyle)
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.insertSubnode(subnode, belowSubnode: navigationBar)
}
}
}, placeholder: placeholderNode)
}
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) {
if let searchDisplayController = self.searchDisplayController {
searchDisplayController.deactivate(placeholder: placeholderNode)
self.searchDisplayController = nil
}
}
func animateIn() {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
private func enqueueTransition(_ transition: ChannelMembersSearchTransition) {
enqueuedTransitions.append(transition)
if self.containerLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
let options = ListViewDeleteAndInsertOptions()
if transition.initial {
//options.insert(.Synchronous)
//options.insert(.LowLatency)
} else {
//options.insert(.AnimateTopItemPosition)
//options.insert(.AnimateCrossfade)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
}
func scrollToTop() {
self.listNode.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 })
}
}
@@ -0,0 +1,135 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import ComponentFlow
import ButtonComponent
import EdgeEffect
import SolidRoundedButtonNode
final class ChannelParticipantFooterItem: ItemListControllerFooterItem {
let theme: PresentationTheme
let title: String
let displayProgress: Bool
let action: () -> Void
init(theme: PresentationTheme, title: String, displayProgress: Bool = false, action: @escaping () -> Void) {
self.theme = theme
self.title = title
self.displayProgress = displayProgress
self.action = action
}
func isEqual(to: ItemListControllerFooterItem) -> Bool {
if let item = to as? ChannelParticipantFooterItem {
return self.theme === item.theme && self.title == item.title && self.displayProgress == item.displayProgress
} else {
return false
}
}
func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode {
if let current = current as? ChannelParticipantFooterItemNode {
current.item = self
return current
} else {
return ChannelParticipantFooterItemNode(item: self)
}
}
}
final class ChannelParticipantFooterItemNode: ItemListControllerFooterItemNode {
private let edgeEffectView = EdgeEffectView()
private let button = ComponentView<Empty>()
private var validLayout: ContainerViewLayout?
var item: ChannelParticipantFooterItem {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
}
init(item: ChannelParticipantFooterItem) {
self.item = item
super.init()
self.updateItem()
}
private func updateItem() {
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
}
}
override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = layout
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let height: CGFloat = 52.0 + buttonInsets.bottom
let edgeEffectHeight: CGFloat = height
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(
content: self.item.theme.list.plainBackgroundColor,
blur: true,
rect: edgeEffectFrame,
edge: .bottom,
edgeSize: edgeEffectFrame.height,
transition: ComponentTransition(transition)
)
if self.edgeEffectView.superview == nil {
self.view.addSubview(self.edgeEffectView)
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: self.item.theme.list.itemCheckColors.fillColor,
foreground: self.item.theme.list.itemCheckColors.foregroundColor,
pressedColor: self.item.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(Text(text: self.item.title, font: Font.semibold(17.0), color: self.item.theme.list.itemCheckColors.foregroundColor))
),
displaysProgress: self.item.displayProgress,
action: { [weak self] in
self?.item.action()
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInsets.left - buttonInsets.right, height: 52.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInsets.left, y: layout.size.height - buttonInsets.bottom - buttonSize.height), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.view.addSubview(buttonView)
}
transition.updateFrame(view: buttonView, frame: buttonFrame)
}
return height
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.edgeEffectView.frame.contains(point) {
return true
} else {
return false
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,323 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
class ChatSlowmodeItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle, value: Int32, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatSlowmodeItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatSlowmodeItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let allowedValues: [Int32] = [0, 5, 10, 30, 60, 300, 900, 3600]
class ChatSlowmodeItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: ChatSlowmodeItem?
private var layoutParams: ListViewItemLayoutParams?
private var reportedValue: Int32?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.textNodes = allowedValues.map { _ -> TextNode in
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
return textNode
}
super.init(layerBacked: false)
self.textNodes.forEach(self.addSubnode)
}
func forceSetValue(_ value: Int32) {
if let sliderView = self.sliderView {
sliderView.value = CGFloat(value)
}
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = CGFloat(allowedValues.count - 1)
sliderView.positionsCount = allowedValues.count
var value: Int32 = 0
for i in 0 ..< allowedValues.count {
if allowedValues[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
sliderView.isUserInteractionEnabled = true
sliderView.alpha = 1.0
sliderView.layer.allowsGroupOpacity = false
}
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
let sliderView = TGPhotoEditorSliderView()
sliderView.limitValueChangedToLatestState = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = CGFloat(allowedValues.count - 1)
sliderView.positionsCount = allowedValues.count
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
var value: Int32 = 0
for i in 0 ..< allowedValues.count {
if allowedValues[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
self.reportedValue = item.value
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: ChatSlowmodeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayouts = self.textNodes.map(TextNode.asyncLayout)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
var textLayoutAndApply: [(TextNodeLayout, () -> TextNode)] = []
for i in 0 ..< allowedValues.count {
let value = allowedValues[i]
let valueString: String
if value == 0 {
valueString = item.strings.GroupInfo_Permissions_SlowmodeValue_Off
} else {
valueString = shortTimeIntervalString(strings: item.strings, value: value)
}
let (textLayout, textApply) = makeTextLayouts[i](TextNodeLayoutArguments(attributedString: NSAttributedString(string: valueString, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayoutAndApply.append((textLayout, textApply))
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeOffset = 0.0
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
for (_, apply) in textLayoutAndApply {
let _ = apply()
}
let textNodes: [(TextNode, CGSize)] = textLayoutAndApply.map { layout, apply -> (TextNode, CGSize) in
let node = apply()
return (node, layout.size)
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let item = self.item, let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value: Int32 = allowedValues[max(0, min(allowedValues.count - 1, position))]
if self.reportedValue != value {
self.reportedValue = value
item.updated(value)
}
}
}
@@ -0,0 +1,345 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
class ChatUnrestrictBoostersItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: Int32, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatUnrestrictBoostersItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatUnrestrictBoostersItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let allowedValues: [Int32] = [1, 2, 3, 4, 5]
class ChatUnrestrictBoostersItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let iconNodes: [ASImageNode]
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: ChatUnrestrictBoostersItem?
private var layoutParams: ListViewItemLayoutParams?
private var reportedValue: Int32?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNodes = allowedValues.map { _ -> ASImageNode in
let iconNode = ASImageNode()
iconNode.isUserInteractionEnabled = false
iconNode.displaysAsynchronously = false
return iconNode
}
self.textNodes = allowedValues.map { _ -> TextNode in
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
return textNode
}
super.init(layerBacked: false)
self.iconNodes.forEach(self.addSubnode)
self.textNodes.forEach(self.addSubnode)
}
func forceSetValue(_ value: Int32) {
if let sliderView = self.sliderView {
sliderView.value = CGFloat(value)
}
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = CGFloat(allowedValues.count - 1)
sliderView.positionsCount = allowedValues.count
var value: Int32 = 0
for i in 0 ..< allowedValues.count {
if allowedValues[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
sliderView.isUserInteractionEnabled = true
sliderView.alpha = 1.0
sliderView.layer.allowsGroupOpacity = false
}
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
let sliderView = TGPhotoEditorSliderView()
sliderView.limitValueChangedToLatestState = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = CGFloat(allowedValues.count - 1)
sliderView.positionsCount = allowedValues.count
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
var value: Int32 = 0
for i in 0 ..< allowedValues.count {
if allowedValues[i] >= item.value {
value = Int32(i)
break
}
}
sliderView.value = CGFloat(value)
self.reportedValue = item.value
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: ChatUnrestrictBoostersItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayouts = self.textNodes.map(TextNode.asyncLayout)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
var textLayoutAndApply: [(TextNodeLayout, () -> TextNode)] = []
for i in 0 ..< allowedValues.count {
let value = allowedValues[i]
let valueString: String
if value == 0 {
valueString = item.strings.GroupInfo_Permissions_SlowmodeValue_Off
} else {
valueString = "\(value)" //shortTimeIntervalString(strings: item.strings, value: value)
}
let (textLayout, textApply) = makeTextLayouts[i](TextNodeLayoutArguments(attributedString: NSAttributedString(string: valueString, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayoutAndApply.append((textLayout, textApply))
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeOffset = 0.0
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
for (_, apply) in textLayoutAndApply {
let _ = apply()
}
let textNodes: [(TextNode, CGSize)] = textLayoutAndApply.map { layout, apply -> (TextNode, CGSize) in
let node = apply()
return (node, layout.size)
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let iconNode = strongSelf.iconNodes[i]
let (textNode, textSize) = textNodes[i]
if themeUpdated {
iconNode.image = generateTintedImage(image: i == 0 ? UIImage(bundleImageName: "Chat/Message/Boost") : UIImage(bundleImageName: "Chat/Message/Boosts"), color: item.theme.list.itemSecondaryTextColor)
}
var position = params.leftInset + 18.0 + delta * CGFloat(i)
var iconOffset: CGFloat = 9.0
let textOffset: CGFloat = 5.0
if i == textNodes.count - 1 {
position -= textSize.width + 8.0
} else if i > 0 {
position -= textSize.width / 2.0
} else if i == 0 {
position += textSize.width
iconOffset = 6.0
}
if let icon = iconNode.image {
iconNode.frame = CGRect(origin: CGPoint(x: position - iconOffset, y: 16.0), size: icon.size)
textNode.frame = CGRect(origin: CGPoint(x: position + textOffset, y: 15.0), size: textSize)
}
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let item = self.item, let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value: Int32 = allowedValues[max(0, min(allowedValues.count - 1, position))]
if self.reportedValue != value {
self.reportedValue = value
item.updated(value)
}
}
}
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
private final class ConvertToSupergroupArguments {
let convert: () -> Void
init(convert: @escaping () -> Void) {
self.convert = convert
}
}
private enum ConvertToSupergroupSection: Int32 {
case info
case action
}
private enum ConvertToSupergroupEntry: ItemListNodeEntry {
case info(PresentationTheme, String)
case action(PresentationTheme, String)
case actionInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .info:
return ConvertToSupergroupSection.info.rawValue
case .action, .actionInfo:
return ConvertToSupergroupSection.action.rawValue
}
}
var stableId: Int32 {
switch self {
case .info:
return 0
case .action:
return 1
case .actionInfo:
return 2
}
}
static func ==(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .action(lhsTheme, lhsText):
if case let .action(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .actionInfo(lhsTheme, lhsText):
if case let .actionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: ConvertToSupergroupEntry, rhs: ConvertToSupergroupEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ConvertToSupergroupArguments
switch self {
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .action(_, title):
return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.convert()
})
case let .actionInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
private struct ConvertToSupergroupState: Equatable {
let isConverting: Bool
init() {
self.isConverting = false
}
init(isConverting: Bool) {
self.isConverting = isConverting
}
static func ==(lhs: ConvertToSupergroupState, rhs: ConvertToSupergroupState) -> Bool {
if lhs.isConverting != rhs.isConverting {
return false
}
return true
}
}
private func convertToSupergroupEntries(presentationData: PresentationData) -> [ConvertToSupergroupEntry] {
var entries: [ConvertToSupergroupEntry] = []
entries.append(.info(presentationData.theme, "\(presentationData.strings.ConvertToSupergroup_HelpTitle)\n\(presentationData.strings.ConvertToSupergroup_HelpText)"))
entries.append(.action(presentationData.theme, presentationData.strings.GroupInfo_ConvertToSupergroup))
entries.append(.actionInfo(presentationData.theme, presentationData.strings.ConvertToSupergroup_Note))
return entries
}
public func convertToSupergroupController(context: AccountContext, peerId: EnginePeer.Id) -> ViewController {
var replaceControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let statePromise = ValuePromise(ConvertToSupergroupState(), ignoreRepeated: true)
let stateValue = Atomic(value: ConvertToSupergroupState())
let updateState: ((ConvertToSupergroupState) -> ConvertToSupergroupState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let actionsDisposable = DisposableSet()
let convertDisposable = MetaDisposable()
actionsDisposable.add(convertDisposable)
let arguments = ConvertToSupergroupArguments(convert: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Group_UpgradeConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
var alreadyConverting = false
updateState { state in
if state.isConverting {
alreadyConverting = true
}
return ConvertToSupergroupState(isConverting: true)
}
if !alreadyConverting {
convertDisposable.set((context.engine.peers.convertGroupToSupergroup(peerId: peerId)
|> deliverOnMainQueue).start(next: { createdPeerId in
replaceControllerImpl?(context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: createdPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil))
}))
}
})]), nil)
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightNavigationButton: ItemListNavigationButton?
if state.isConverting {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ConvertToSupergroup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: convertToSupergroupEntries(presentationData: presentationData), style: .blocks)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
replaceControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: true)
}
}
presentControllerImpl = { [weak controller] value, presentationArguments in
controller?.present(value, in: .window(.root), with: presentationArguments)
}
return controller
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import ItemListUI
import PresentationDataUtils
import AccountContext
final class ChannelMembersSearchItem: ItemListControllerSearch {
let context: AccountContext
let peerId: EnginePeer.Id
let searchContext: GroupMembersSearchContext?
let cancel: () -> Void
let openPeer: (EnginePeer, RenderedChannelParticipant?) -> Void
let pushController: (ViewController) -> Void
let dismissInput: () -> Void
let searchMode: ChannelMembersSearchMode
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, peerId: EnginePeer.Id, searchContext: GroupMembersSearchContext?, searchMode: ChannelMembersSearchMode = .searchMembers, cancel: @escaping () -> Void, openPeer: @escaping (EnginePeer, RenderedChannelParticipant?) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.peerId = peerId
self.searchContext = searchContext
self.cancel = cancel
self.openPeer = openPeer
self.pushController = pushController
self.dismissInput = dismissInput
self.searchMode = searchMode
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
} else {
return .single(value)
}
}).start(next: { [weak self] value in
self?.updateActivity?(value)
}))
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? ChannelMembersSearchItem {
if self.context !== to.context {
return false
}
if self.peerId != to.peerId {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let current = current as? GroupInfoSearchNavigationContentNode {
current.updateTheme(presentationData.theme)
return current
} else {
return GroupInfoSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, mode: self.searchMode, cancel: self.cancel, updateActivity: { [weak self] value in
self?.updateActivity = value
})
}
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return ChannelMembersSearchItemNode(context: self.context, peerId: self.peerId, searchMode: self.searchMode, searchContext: self.searchContext, openPeer: self.openPeer, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, pushController: { [weak self] c in
self?.pushController(c)
}, dismissInput: self.dismissInput)
}
}
private final class ChannelMembersSearchItemNode: ItemListControllerSearchNode {
private let containerNode: ChannelMembersSearchContainerNode
init(context: AccountContext, peerId: EnginePeer.Id, searchMode: ChannelMembersSearchMode, searchContext: GroupMembersSearchContext?, openPeer: @escaping (EnginePeer, RenderedChannelParticipant?) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, dismissInput: @escaping () -> Void) {
self.containerNode = ChannelMembersSearchContainerNode(context: context, forceTheme: nil, peerId: peerId, mode: searchMode, filters: [], searchContext: searchContext, openPeer: { peer, participant in
openPeer(peer, participant)
}, updateActivity: updateActivity, pushController: pushController)
self.containerNode.cancel = {
cancel()
}
super.init()
self.addSubnode(self.containerNode)
self.containerNode.dismissInput = {
dismissInput()
}
}
override func queryUpdated(_ query: String) {
self.containerNode.searchTextUpdated(text: query)
}
override func scrollToTop() {
self.containerNode.scrollToTop()
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height)), navigationBarHeight: navigationBarHeight, transition: transition)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
@@ -0,0 +1,238 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SearchBarNode
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ActivityIndicator
private let searchBarFont = Font.regular(17.0)
final class GroupInfoSearchNavigationContentNode: NavigationBarContentNode, ItemListControllerSearchNavigationContentNode {
private struct Params: Equatable {
let size: CGSize
let leftInset: CGFloat
let rightInset: CGFloat
init(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
self.size = size
self.leftInset = leftInset
self.rightInset = rightInset
}
}
private var theme: PresentationTheme
private let strings: PresentationStrings
private let searchMode: ChannelMembersSearchMode
private let cancel: () -> Void
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let iconView: UIImageView
private var activityIndicator: ActivityIndicator?
private let searchBar: SearchBarNode
private let close: (background: GlassBackgroundView, icon: UIImageView)
private var params: Params?
private var queryUpdated: ((String) -> Void)?
var activity: Bool = false {
didSet {
if self.activity != oldValue {
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
}
}
}
init(theme: PresentationTheme, strings: PresentationStrings, mode: ChannelMembersSearchMode, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
self.theme = theme
self.strings = strings
self.searchMode = mode
self.cancel = cancel
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.iconView = UIImageView()
self.backgroundView.contentView.addSubview(self.iconView)
self.close = (GlassBackgroundView(), UIImageView())
self.close.background.contentView.addSubview(self.close.icon)
self.searchBar = SearchBarNode(
theme: SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: theme.chat.inputPanel.panelControlColor,
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: theme.chat.inputPanel.inputControlColor,
inputClear: theme.chat.inputPanel.panelControlColor,
accent: theme.chat.inputPanel.panelControlAccentColor,
keyboard: theme.rootController.keyboardColor
),
presentationTheme: theme,
strings: strings,
fieldStyle: .inlineNavigation,
forceSeparator: false,
displayBackground: false,
cancelText: nil
)
super.init()
self.view.addSubview(self.backgroundContainer)
self.backgroundView.contentView.addSubview(self.searchBar.view)
self.backgroundContainer.contentView.addSubview(self.close.background)
self.close.background.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.cancel()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
updateActivity({ [weak self] value in
self?.activity = value
})
self.updatePlaceholder()
}
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.searchBar.cancel?()
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
self.updatePlaceholder()
}
func updatePlaceholder() {
let placeholderText: String
switch self.searchMode {
case .searchBanned:
placeholderText = self.strings.GroupInfo_Permissions_SearchPlaceholder
default:
placeholderText = self.strings.Conversation_SearchByName_Placeholder
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: self.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
}
override var nominalHeight: CGFloat {
return 60.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
self.params = Params(size: size, leftInset: leftInset, rightInset: rightInset)
let transition = ComponentTransition(transition)
let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 6.0), size: CGSize(width: size.width - 16.0 * 2.0 - leftInset - rightInset - 44.0 - 8.0, height: 44.0))
let closeFrame = CGRect(origin: CGPoint(x: size.width - 16.0 - rightInset - 44.0, y: backgroundFrame.minY), size: CGSize(width: 44.0, height: 44.0))
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundContainer.update(size: size, isDark: self.theme.overallDarkAppearance, transition: transition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
}
transition.setTintColor(view: self.iconView, color: self.theme.rootController.navigationSearchBar.inputIconColor)
if let image = self.iconView.image {
let imageSize: CGSize
let iconFrame: CGRect
let iconFraction: CGFloat = 0.8
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((backgroundFrame.height - imageSize.height) * 0.5)), size: imageSize)
transition.setPosition(view: self.iconView, position: iconFrame.center)
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if self.activity {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.theme.chat.inputPanel.inputControlColor, 14.0, 14.0, false))
self.activityIndicator = activityIndicator
self.backgroundView.contentView.addSubview(activityIndicator.view)
}
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
let indicatorFrame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((backgroundFrame.height - indicatorSize.height) * 0.5)), size: indicatorSize)
transition.setPosition(view: activityIndicator.view, position: indicatorFrame.center)
transition.setBounds(view: activityIndicator.view, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
} else if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil
activityIndicator.view.removeFromSuperview()
}
self.iconView.isHidden = self.activity
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundFrame.width - 36.0 - 4.0, height: 44.0))
transition.setFrame(view: self.searchBar.view, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
if self.close.icon.image == nil {
self.close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 12.0, y: 12.0))
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
context.strokePath()
})?.withRenderingMode(.alwaysTemplate)
}
if let image = close.icon.image {
self.close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
}
self.close.icon.tintColor = self.theme.chat.inputPanel.panelControlColor
transition.setFrame(view: self.close.background, frame: closeFrame)
self.close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
return size
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
@@ -0,0 +1,223 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import OldChannelsController
private final class GroupPreHistorySetupArguments {
let toggle: (Bool) -> Void
init(toggle: @escaping (Bool) -> Void) {
self.toggle = toggle
}
}
private enum GroupPreHistorySetupSection: Int32 {
case info
}
private enum GroupPreHistorySetupEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case visible(PresentationTheme, String, Bool)
case hidden(PresentationTheme, String, Bool)
case info(PresentationTheme, String)
var section: ItemListSectionId {
return GroupPreHistorySetupSection.info.rawValue
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .visible:
return 1
case .hidden:
return 2
case .info:
return 3
}
}
static func ==(lhs: GroupPreHistorySetupEntry, rhs: GroupPreHistorySetupEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsText):
if case let .header(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .visible(lhsTheme, lhsText, lhsValue):
if case let .visible(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .hidden(lhsTheme, lhsText, lhsValue):
if case let .hidden(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: GroupPreHistorySetupEntry, rhs: GroupPreHistorySetupEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GroupPreHistorySetupArguments
switch self {
case let .header(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .visible(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggle(true)
})
case let .hidden(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggle(false)
})
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
private struct GroupPreHistorySetupState: Equatable {
var changedValue: Bool?
var applyingSetting: Bool = false
}
private func groupPreHistorySetupEntries(isSupergroup: Bool, presentationData: PresentationData, defaultValue: Bool, state: GroupPreHistorySetupState) -> [GroupPreHistorySetupEntry] {
var entries: [GroupPreHistorySetupEntry] = []
let value = state.changedValue ?? defaultValue
entries.append(.header(presentationData.theme, presentationData.strings.Group_Setup_HistoryHeader))
entries.append(.visible(presentationData.theme, presentationData.strings.Group_Setup_HistoryVisible, value))
entries.append(.hidden(presentationData.theme, presentationData.strings.Group_Setup_HistoryHidden, !value))
if isSupergroup {
entries.append(.info(presentationData.theme, value ? presentationData.strings.Group_Setup_HistoryVisibleHelp : presentationData.strings.Group_Setup_HistoryHiddenHelp))
} else {
entries.append(.info(presentationData.theme, value ? presentationData.strings.Group_Setup_HistoryVisibleHelp : presentationData.strings.Group_Setup_BasicHistoryHiddenHelp))
}
return entries
}
public func groupPreHistorySetupController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void) -> ViewController {
let statePromise = ValuePromise(GroupPreHistorySetupState(), ignoreRepeated: true)
let stateValue = Atomic(value: GroupPreHistorySetupState())
let updateState: ((GroupPreHistorySetupState) -> GroupPreHistorySetupState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let applyDisposable = MetaDisposable()
actionsDisposable.add(applyDisposable)
let arguments = GroupPreHistorySetupArguments(toggle: { value in
updateState { state in
var state = state
state.changedValue = value
return state
}
})
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(presentationData, statePromise.get(), context.account.viewTracker.peerView(peerId))
|> deliverOnMainQueue
|> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, Any)) in
let defaultValue: Bool = (view.cachedData as? CachedChannelData)?.flags.contains(.preHistoryEnabled) ?? false
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var rightNavigationButton: ItemListNavigationButton?
if state.applyingSetting {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
var value: Bool?
updateState { state in
var state = state
state.applyingSetting = true
value = state.changedValue
return state
}
if let value = value, value != defaultValue {
if peerId.namespace == Namespaces.Peer.CloudGroup {
let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId)
|> mapToSignal { upgradedPeerId -> Signal<PeerId?, ConvertGroupToSupergroupError> in
return context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: upgradedPeerId, historyAvailableForNewMembers: value)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<PeerId?, NoError> in
return .complete()
}
|> then(.single(upgradedPeerId))
|> castError(ConvertGroupToSupergroupError.self)
}
|> deliverOnMainQueue
applyDisposable.set((signal
|> deliverOnMainQueue).start(next: { upgradedPeerId in
if let upgradedPeerId = upgradedPeerId {
upgradedToSupergroup(upgradedPeerId, {
dismissImpl?()
})
}
}, error: { error in
switch error {
case .tooManyChannels:
pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade))
default:
break
}
}))
} else {
applyDisposable.set((context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: peerId, historyAvailableForNewMembers: value)
|> deliverOnMainQueue).start(completed: {
dismissImpl?()
}))
}
} else {
dismissImpl?()
}
})
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Group_Setup_HistoryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupPreHistorySetupEntries(isSupergroup: peerId.namespace == Namespaces.Peer.CloudChannel, presentationData: presentationData, defaultValue: defaultValue, state: state), style: .blocks)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
return controller
}
@@ -0,0 +1,384 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import TextFormat
public class ItemListCallListItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let dateTimeFormat: PresentationDateTimeFormat
let messages: [Message]
public let sectionId: ItemListSectionId
let style: ItemListStyle
let displayDecorations: Bool
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, messages: [Message], sectionId: ItemListSectionId, style: ItemListStyle, displayDecorations: Bool = true) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.messages = messages
self.sectionId = sectionId
self.style = style
self.displayDecorations = displayDecorations
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListCallListItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListCallListItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func stringForCallType(message: Message, strings: PresentationStrings) -> String {
var string = ""
for media in message.media {
switch media {
case let action as TelegramMediaAction:
switch action.action {
case let .phoneCall(_, discardReason, _, isVideo):
let incoming = message.flags.contains(.Incoming)
if let discardReason = discardReason {
switch discardReason {
case .disconnect:
if isVideo {
string = strings.Notification_VideoCallCanceled
} else {
string = strings.Notification_CallCanceled
}
case .missed, .busy:
if incoming {
if isVideo {
string = strings.Notification_VideoCallMissed
} else {
string = strings.Notification_CallMissed
}
} else {
if isVideo {
string = strings.Notification_VideoCallCanceled
} else {
string = strings.Notification_CallCanceled
}
}
case .hangup:
break
}
}
if string.isEmpty {
if incoming {
if isVideo {
string = strings.Notification_VideoCallIncoming
} else {
string = strings.Notification_CallIncoming
}
} else {
if isVideo {
string = strings.Notification_VideoCallOutgoing
} else {
string = strings.Notification_CallOutgoing
}
}
}
case let .conferenceCall(conferenceCall):
let incoming = message.flags.contains(.Incoming)
let missedTimeout: Int32 = 30
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
string = strings.Chat_CallMessage_DeclinedGroupCall
} else if conferenceCall.duration == nil && message.timestamp < currentTime - missedTimeout {
string = strings.Chat_CallMessage_MissedGroupCall
} else {
if incoming {
string = strings.Chat_CallMessage_IncomingGroupCall
} else {
string = strings.Chat_CallMessage_OutgoingGroupCall
}
}
default:
break
}
default:
break
}
}
return string
}
public class ItemListCallListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
let titleNode: TextNode
var callNodes: [(TextNode, TextNode)]
private let accessibilityArea: AccessibilityAreaNode
private var item: ItemListCallListItem?
override public var canBeSelected: Bool {
return false
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.isAccessibilityElement = false
self.callNodes = []
self.accessibilityArea = AccessibilityAreaNode()
super.init(layerBacked: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.accessibilityArea)
}
public func asyncLayout() -> (_ item: ItemListCallListItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { [weak self] item, params, neighbors in
if let strongSelf = self, strongSelf.callNodes.count != item.messages.count {
for pair in strongSelf.callNodes {
pair.0.removeFromSupernode()
pair.1.removeFromSupernode()
}
strongSelf.callNodes = []
for _ in item.messages {
let timeNode = TextNode()
timeNode.isUserInteractionEnabled = false
strongSelf.addSubnode(timeNode)
let typeNode = TextNode()
typeNode.isUserInteractionEnabled = false
strongSelf.addSubnode(typeNode)
strongSelf.callNodes.append((timeNode, typeNode))
}
}
var makeNodesLayout: [((TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode))] = []
if let strongSelf = self {
for nodes in strongSelf.callNodes {
let makeTimeLayout = TextNode.asyncLayout(nodes.0)
let makeTypeLayout = TextNode.asyncLayout(nodes.1)
makeNodesLayout.append((makeTimeLayout, makeTypeLayout))
}
}
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let font = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let typeFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let contentSize: CGSize
var contentHeight: CGFloat = 0.0
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
if !item.displayDecorations {
insets = UIEdgeInsets()
}
var accessibilityText = ""
let earliestMessage = item.messages.sorted(by: {$0.timestamp < $1.timestamp}).first!
let titleText = stringForDate(timestamp: earliestMessage.timestamp, strings: item.presentationData.strings)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
accessibilityText.append(titleText)
accessibilityText.append(". ")
contentHeight += titleLayout.size.height + 18.0
var index = 0
var nodesLayout: [(TextNodeLayout, TextNodeLayout)] = []
var nodesApply: [(() -> TextNode, () -> TextNode)] = []
for message in item.messages {
let makeTimeLayout = makeNodesLayout[index].0
let time = stringForMessageTimestamp(timestamp: message.timestamp, dateTimeFormat: item.dateTimeFormat)
let (timeLayout, timeApply) = makeTimeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: time, font: font, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let makeTypeLayout = makeNodesLayout[index].1
let type = stringForCallType(message: message, strings: item.presentationData.strings)
let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: type, font: typeFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
accessibilityText.append("\(time) - \(type).\n")
nodesLayout.append((timeLayout, typeLayout))
nodesApply.append((timeApply, typeApply))
contentHeight += timeLayout.size.height + 12.0
index += 1
}
contentSize = CGSize(width: params.width, height: contentHeight)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let _ = titleApply()
for apply in nodesApply {
let _ = apply.0()
let _ = apply.1()
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = !item.displayDecorations
}
strongSelf.bottomStripeNode.isHidden = !item.displayDecorations
strongSelf.backgroundNode.isHidden = !item.displayDecorations
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: titleLayout.size)
var index = 0
var yOffset = strongSelf.titleNode.frame.maxY + 10.0
for nodes in strongSelf.callNodes {
let layout = nodesLayout[index]
nodes.0.frame = CGRect(origin: CGPoint(x: leftInset, y: yOffset), size: layout.0.size)
nodes.1.frame = CGRect(origin: CGPoint(x: leftInset + 75.0, y: yOffset), size: layout.1.size)
yOffset += layout.0.size.height + 12.0
index += 1
}
strongSelf.accessibilityArea.accessibilityLabel = accessibilityText
strongSelf.accessibilityArea.accessibilityTraits = .staticText
strongSelf.accessibilityArea.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,486 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import SwitchNode
import TelegramCore
import ItemListUI
import ReactionImageComponent
import AccountContext
public class ItemListReactionItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let availableReactions: AvailableReactions?
let reaction: MessageReaction.Reaction
let title: String
let value: Bool
let enabled: Bool
public let sectionId: ItemListSectionId
let style: ItemListStyle
let updated: (Bool) -> Void
public let tag: ItemListItemTag?
public init(
context: AccountContext,
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle = .glass,
availableReactions: AvailableReactions?,
reaction: MessageReaction.Reaction,
title: String,
value: Bool,
enabled: Bool = true,
sectionId: ItemListSectionId,
style: ItemListStyle,
updated: @escaping (Bool) -> Void,
tag: ItemListItemTag? = nil
) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
self.availableReactions = availableReactions
self.reaction = reaction
self.title = title
self.value = value
self.enabled = enabled
self.sectionId = sectionId
self.style = style
self.updated = updated
self.tag = tag
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListReactionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(false) })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListReactionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
var animated = true
if case .None = animation {
animated = false
}
apply(animated)
})
}
}
}
}
}
}
private protocol ItemListSwitchNodeImpl {
var frameColor: UIColor { get set }
var contentColor: UIColor { get set }
var handleColor: UIColor { get set }
var positiveContentColor: UIColor { get set }
var negativeContentColor: UIColor { get set }
var isOn: Bool { get }
func setOn(_ value: Bool, animated: Bool)
}
extension SwitchNode: ItemListSwitchNodeImpl {
var positiveContentColor: UIColor {
get {
return .white
} set(value) {
}
}
var negativeContentColor: UIColor {
get {
return .white
} set(value) {
}
}
}
extension IconSwitchNode: ItemListSwitchNodeImpl {
}
public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let maskNode: ASImageNode
private var imageNode: ReactionImageNode?
private let titleNode: TextNode
private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl
private let switchGestureNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode?
private let activateArea: AccessibilityAreaNode
private var item: ItemListReactionItem?
public var tag: ItemListItemTag? {
return self.item?.tag
}
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.switchNode = SwitchNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.switchGestureNode = ASDisplayNode()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.switchNode)
self.addSubnode(self.switchGestureNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
guard let strongSelf = self, let item = strongSelf.item, item.enabled else {
return false
}
let value = !strongSelf.switchNode.isOn
strongSelf.switchNode.setOn(value, animated: true)
item.updated(value)
return true
}
}
override public func didLoad() {
super.didLoad()
(self.switchNode.view as? UISwitch)?.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged)
self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
var currentDisabledOverlayNode = self.disabledOverlayNode
return { item, params, neighbors in
var contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let sideImageInset: CGFloat = 44.0
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 44.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: 44.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0 - sideImageInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
contentSize.height = max(contentSize.height, titleLayout.size.height + verticalInset * 2.0)
if !item.enabled {
if currentDisabledOverlayNode == nil {
currentDisabledOverlayNode = ASDisplayNode()
}
} else {
currentDisabledOverlayNode = nil
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.value ? item.presentationData.strings.VoiceOver_Common_On : item.presentationData.strings.VoiceOver_Common_Off
strongSelf.activateArea.accessibilityHint = item.presentationData.strings.VoiceOver_Common_SwitchHint
var accessibilityTraits = UIAccessibilityTraits()
if item.enabled {
} else {
accessibilityTraits.insert(.notEnabled)
}
strongSelf.activateArea.accessibilityTraits = accessibilityTraits
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
strongSelf.insertSubnode(currentDisabledOverlayNode, belowSubnode: strongSelf.switchGestureNode)
currentDisabledOverlayNode.alpha = 0.0
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
} else {
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
}
currentDisabledOverlayNode.backgroundColor = itemBackgroundColor.withAlphaComponent(0.6)
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
disabledOverlayNode?.removeFromSupernode()
})
strongSelf.disabledOverlayNode = nil
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor
strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor
strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor
strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor
strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
let leftInset = 16.0 + params.leftInset + sideImageInset
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, aboveSubnode: strongSelf.switchGestureNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset + sideImageInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))
}
if strongSelf.imageNode == nil, let availableReactions = item.availableReactions {
let imageNode = ReactionImageNode(context: item.context, availableReactions: availableReactions, reaction: item.reaction, displayPixelSize: CGSize(width: 30.0 * UIScreenScale, height: 30.0 * UIScreenScale))
strongSelf.imageNode = imageNode
strongSelf.addSubnode(imageNode)
}
if let imageNode = strongSelf.imageNode {
let imageFitSize = CGSize(width: 30.0, height: 30.0)
imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor(sideImageInset - imageFitSize.width), y: floor((contentSize.height - imageFitSize.height) / 2.0)), size: imageFitSize)
imageNode.update(size: imageFitSize)
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
if let switchView = strongSelf.switchNode.view as? UISwitch {
if strongSelf.switchNode.bounds.size.width.isZero {
switchView.sizeToFit()
}
let switchSize = switchView.bounds.size
strongSelf.switchNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - switchSize.width - 15.0, y: floor((contentSize.height - switchSize.height) / 2.0)), size: switchSize)
strongSelf.switchGestureNode.frame = strongSelf.switchNode.frame
if switchView.isOn != item.value {
switchView.setOn(item.value, animated: animated)
}
switchView.isUserInteractionEnabled = true
}
strongSelf.switchGestureNode.isHidden = item.enabled
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel))
}
})
}
}
override public func accessibilityActivate() -> Bool {
guard let item = self.item else {
return false
}
if !item.enabled {
return false
}
if let switchNode = self.switchNode as? IconSwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
} else if let switchNode = self.switchNode as? SwitchNode {
switchNode.isOn = !switchNode.isOn
item.updated(switchNode.isOn)
}
return true
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func switchValueChanged(_ switchView: UISwitch) {
if let item = self.item {
let value = switchView.isOn
item.updated(value)
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if let item = self.item, let switchView = self.switchNode.view as? UISwitch, case .ended = recognizer.state {
if item.enabled {
let value = switchView.isOn
item.updated(!value)
}
}
}
}
@@ -0,0 +1,342 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import EncryptionKeyVisualization
class ItemListSecretChatKeyItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let icon: UIImage?
let title: String
let fingerprint: SecretChatKeyFingerprint
let sectionId: ItemListSectionId
let style: ItemListStyle
let disclosureStyle: ItemListDisclosureStyle
let action: (() -> Void)?
init(presentationData: ItemListPresentationData, icon: UIImage? = nil, title: String, fingerprint: SecretChatKeyFingerprint, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.title = title
self.fingerprint = fingerprint
self.sectionId = sectionId
self.style = style
self.disclosureStyle = disclosureStyle
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListSecretChatKeyItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListSecretChatKeyItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action?()
}
}
class ItemListSecretChatKeyItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
let iconNode: ASImageNode
let titleNode: TextNode
let keyNode: ASImageNode
let arrowNode: ASImageNode
private var item: ItemListSecretChatKeyItem?
override var canBeSelected: Bool {
if let item = self.item, let _ = item.action {
return true
} else {
return false
}
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.keyNode = ASImageNode()
self.keyNode.isLayerBacked = true
self.keyNode.displayWithoutProcessing = true
self.keyNode.displaysAsynchronously = false
self.arrowNode = ASImageNode()
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.arrowNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.keyNode)
self.addSubnode(self.arrowNode)
}
func asyncLayout() -> (_ item: ItemListSecretChatKeyItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
let rightInset: CGFloat
switch item.disclosureStyle {
case .none:
rightInset = 16.0 + params.rightInset
case .arrow, .optionArrows:
rightInset = 34.0 + params.rightInset
}
var updateArrowImage: UIImage?
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
var updateIcon = false
if currentItem?.icon != item.icon {
updateIcon = true
}
var updateKeyImage: UIImage?
if currentItem?.fingerprint != item.fingerprint {
updateKeyImage = secretChatKeyImage(item.fingerprint, size: CGSize(width: 24.0, height: 24.0))
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
var leftInset = 16.0 + params.leftInset
if let _ = item.icon {
leftInset += 43.0
}
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let icon = item.icon {
if strongSelf.iconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconNode)
}
if updateIcon {
strongSelf.iconNode.image = icon
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - icon.size.width) / 2.0), y: floor((layout.contentSize.height - icon.size.height) / 2.0)), size: icon.size)
} else if strongSelf.iconNode.supernode != nil {
strongSelf.iconNode.image = nil
strongSelf.iconNode.removeFromSupernode()
}
if let updateArrowImage = updateArrowImage {
strongSelf.arrowNode.image = updateArrowImage
}
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let _ = titleApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
if let updateKeyImage = updateKeyImage {
strongSelf.keyNode.image = updateKeyImage
}
if let image = strongSelf.keyNode.image {
strongSelf.keyNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - image.size.width, y: floor((layout.contentSize.height - image.size.height) / 2.0)), size: image.size)
}
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: 15.0), size: arrowImage.size)
}
switch item.disclosureStyle {
case .none:
strongSelf.arrowNode.isHidden = true
case .arrow, .optionArrows:
strongSelf.arrowNode.isHidden = false
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,557 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import PresentationDataUtils
private enum PeerReactionsMode {
case all
case some
case empty
}
private final class PeerAllowedReactionListControllerArguments {
let context: AccountContext
let setMode: (PeerReactionsMode, Bool) -> Void
let toggleItem: (MessageReaction.Reaction) -> Void
init(
context: AccountContext,
setMode: @escaping (PeerReactionsMode, Bool) -> Void,
toggleItem: @escaping (MessageReaction.Reaction) -> Void
) {
self.context = context
self.setMode = setMode
self.toggleItem = toggleItem
}
}
private enum PeerAllowedReactionListControllerSection: Int32 {
case all
case items
}
private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry {
enum StableId: Hashable {
case allowSwitch
case allowAllHeader
case allowAll
case allowSome
case allowNone
case allowAllInfo
case itemsHeader
case item(MessageReaction.Reaction)
}
case allowSwitch(text: String, value: Bool)
case allowAllHeader(String)
case allowAll(text: String, isEnabled: Bool)
case allowSome(text: String, isEnabled: Bool)
case allowNone(text: String, isEnabled: Bool)
case allowAllInfo(String)
case itemsHeader(String)
case item(index: Int, value: MessageReaction.Reaction, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, text: String, isEnabled: Bool, allDisabled: Bool)
var section: ItemListSectionId {
switch self {
case .allowSwitch, .allowAllHeader, .allowAll, .allowSome, .allowNone, .allowAllInfo:
return PeerAllowedReactionListControllerSection.all.rawValue
case .itemsHeader, .item:
return PeerAllowedReactionListControllerSection.items.rawValue
}
}
var stableId: StableId {
switch self {
case .allowSwitch:
return .allowSwitch
case .allowAllHeader:
return .allowAllHeader
case .allowAll:
return .allowAll
case .allowSome:
return .allowSome
case .allowNone:
return .allowNone
case .allowAllInfo:
return .allowAllInfo
case .itemsHeader:
return .itemsHeader
case let .item(_, value, _, _, _, _, _):
return .item(value)
}
}
var sortId: Int {
switch self {
case .allowSwitch:
return 0
case .allowAllHeader:
return 1
case .allowAll:
return 2
case .allowSome:
return 3
case .allowNone:
return 4
case .allowAllInfo:
return 5
case .itemsHeader:
return 6
case let .item(index, _, _, _, _, _, _):
return 100 + index
}
}
static func ==(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool {
switch lhs {
case let .allowSwitch(text, value):
if case .allowSwitch(text, value) = rhs {
return true
} else {
return false
}
case let .allowAllHeader(text):
if case .allowAllHeader(text) = rhs {
return true
} else {
return false
}
case let .allowAll(text, isEnabled):
if case .allowAll(text, isEnabled) = rhs {
return true
} else {
return false
}
case let .allowSome(text, isEnabled):
if case .allowSome(text, isEnabled) = rhs {
return true
} else {
return false
}
case let .allowNone(text, isEnabled):
if case .allowNone(text, isEnabled) = rhs {
return true
} else {
return false
}
case let .allowAllInfo(text):
if case .allowAllInfo(text) = rhs {
return true
} else {
return false
}
case let .itemsHeader(text):
if case .itemsHeader(text) = rhs {
return true
} else {
return false
}
case let .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled):
if case .item(index, value, availableReactions, reaction, text, isEnabled, allDisabled) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: PeerAllowedReactionListControllerEntry, rhs: PeerAllowedReactionListControllerEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PeerAllowedReactionListControllerArguments
switch self {
case let .allowSwitch(text, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
if value {
arguments.setMode(.some, false)
} else {
arguments.setMode(.empty, false)
}
})
case let .allowAllHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .allowAll(text, isEnabled):
return ItemListCheckboxItem(
presentationData: presentationData,
systemStyle: .glass,
icon: nil,
iconSize: nil,
iconPlacement: .default,
title: text,
subtitle: nil,
style: .right,
color: .accent,
textColor: .primary,
checked: isEnabled,
zeroSeparatorInsets: false,
sectionId: self.section,
action: {
arguments.setMode(.all, true)
},
deleteAction: nil
)
case let .allowSome(text, isEnabled):
return ItemListCheckboxItem(
presentationData: presentationData,
systemStyle: .glass,
icon: nil,
iconSize: nil,
iconPlacement: .default,
title: text,
subtitle: nil,
style: .right,
color: .accent,
textColor: .primary,
checked: isEnabled,
zeroSeparatorInsets: false,
sectionId: self.section,
action: {
arguments.setMode(.some, true)
},
deleteAction: nil
)
case let .allowNone(text, isEnabled):
return ItemListCheckboxItem(
presentationData: presentationData,
systemStyle: .glass,
icon: nil,
iconSize: nil,
iconPlacement: .default,
title: text,
subtitle: nil,
style: .right,
color: .accent,
textColor: .primary,
checked: isEnabled,
zeroSeparatorInsets: false,
sectionId: self.section,
action: {
arguments.setMode(.empty, true)
},
deleteAction: nil
)
case let .allowAllInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .itemsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .item(_, value, availableReactions, reaction, text, isEnabled, allDisabled):
return ItemListReactionItem(
context: arguments.context,
presentationData: presentationData,
systemStyle: .glass,
availableReactions: availableReactions,
reaction: reaction,
title: text,
value: isEnabled,
enabled: !allDisabled,
sectionId: self.section,
style: .blocks,
updated: { _ in
arguments.toggleItem(value)
}
)
}
}
}
private struct PeerAllowedReactionListControllerState: Equatable {
var updatedMode: PeerReactionsMode?
var updatedAllowedReactions: Set<MessageReaction.Reaction>? = nil
}
private func peerAllowedReactionListControllerEntries(
presentationData: PresentationData,
availableReactions: AvailableReactions?,
peer: Peer?,
cachedData: CachedPeerData?,
state: PeerAllowedReactionListControllerState
) -> [PeerAllowedReactionListControllerEntry] {
var entries: [PeerAllowedReactionListControllerEntry] = []
if let peer = peer, let availableReactions = availableReactions, let allowedReactions = state.updatedAllowedReactions, let mode = state.updatedMode {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
entries.append(.allowSwitch(text: presentationData.strings.PeerInfo_AllowedReactions_AllowAllText, value: mode != .empty))
entries.append(.itemsHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader))
var index = 0
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value), allDisabled: mode == .empty))
index += 1
}
} else {
entries.append(.allowAllHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader))
entries.append(.allowAll(text: presentationData.strings.PeerInfo_AllowedReactions_OptionAllReactions, isEnabled: mode == .all))
entries.append(.allowSome(text: presentationData.strings.PeerInfo_AllowedReactions_OptionSomeReactions, isEnabled: mode == .some))
entries.append(.allowNone(text: presentationData.strings.PeerInfo_AllowedReactions_OptionNoReactions, isEnabled: mode == .empty))
let allInfoText: String
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
switch mode {
case .all:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionAllInfo
case .some:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionSomeInfo
case .empty:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionNoInfo
}
} else {
switch mode {
case .all:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionAllInfo
case .some:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionSomeInfo
case .empty:
allInfoText = presentationData.strings.PeerInfo_AllowedReactions_GroupOptionNoInfo
}
}
entries.append(.allowAllInfo(allInfoText))
if mode == .some {
entries.append(.itemsHeader(presentationData.strings.PeerInfo_AllowedReactions_ReactionListHeader))
var index = 0
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value), allDisabled: false))
index += 1
}
}
}
}
return entries
}
public func peerAllowedReactionListController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
peerId: PeerId
) -> ViewController {
let statePromise = ValuePromise(PeerAllowedReactionListControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeerAllowedReactionListControllerState())
let updateState: ((PeerAllowedReactionListControllerState) -> PeerAllowedReactionListControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
let _ = dismissImpl
let actionsDisposable = DisposableSet()
actionsDisposable.add((combineLatest(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)), context.engine.stickers.availableReactions() |> take(1))
|> deliverOnMainQueue).start(next: { allowedReactions, availableReactions in
updateState { state in
var state = state
switch allowedReactions {
case .unknown:
break
case let .known(value):
switch value {
case .all:
state.updatedMode = .all
if let availableReactions = availableReactions {
state.updatedAllowedReactions = Set(availableReactions.reactions.filter(\.isEnabled).map(\.value))
} else {
state.updatedAllowedReactions = Set()
}
case let .limited(reactions):
state.updatedMode = .some
state.updatedAllowedReactions = Set(reactions)
case .empty:
state.updatedMode = .empty
state.updatedAllowedReactions = Set()
}
}
return state
}
}))
let arguments = PeerAllowedReactionListControllerArguments(
context: context,
setMode: { mode, resetItems in
let _ = (context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { availableReactions in
guard let availableReactions = availableReactions else {
return
}
updateState { state in
var state = state
state.updatedMode = mode
if var updatedAllowedReactions = state.updatedAllowedReactions {
switch mode {
case .all:
if resetItems {
updatedAllowedReactions.removeAll()
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
updatedAllowedReactions.insert(availableReaction.value)
}
}
case .some:
if resetItems {
updatedAllowedReactions.removeAll()
if let thumbsUp = availableReactions.reactions.first(where: { $0.value == .builtin("👍") }) {
updatedAllowedReactions.insert(thumbsUp.value)
}
if let thumbsDown = availableReactions.reactions.first(where: { $0.value == .builtin("👎") }) {
updatedAllowedReactions.insert(thumbsDown.value)
}
} else {
updatedAllowedReactions.removeAll()
for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
updatedAllowedReactions.insert(availableReaction.value)
}
}
case .empty:
if resetItems {
updatedAllowedReactions.removeAll()
}
}
state.updatedAllowedReactions = updatedAllowedReactions
}
return state
}
})
},
toggleItem: { reaction in
updateState { state in
var state = state
if var updatedAllowedReactions = state.updatedAllowedReactions {
if updatedAllowedReactions.contains(reaction) {
updatedAllowedReactions.remove(reaction)
if state.updatedMode == .all {
state.updatedMode = .some
}
} else {
updatedAllowedReactions.insert(reaction)
}
state.updatedAllowedReactions = updatedAllowedReactions
}
return state
}
}
)
let peerView = context.account.viewTracker.peerView(peerId)
|> deliverOnMainQueue
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(),
presentationData,
statePromise.get(),
context.engine.stickers.availableReactions(),
peerView
)
|> deliverOnMainQueue
|> map { presentationData, state, availableReactions, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in
let title: String = presentationData.strings.PeerInfo_AllowedReactions_Title
let entries = peerAllowedReactionListControllerEntries(
presentationData: presentationData,
availableReactions: availableReactions,
peer: peerView.peers[peerId],
cachedData: peerView.cachedData,
state: state
)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(title),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
animateChanges: false
)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.willDisappear = { _ in
let _ = (combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: peerId)
),
context.engine.stickers.availableReactions() |> take(1)
)
|> deliverOnMainQueue).start(next: { data, availableReactions in
let (peer, initialAllowedReactions) = data
guard let peer = peer, let availableReactions = availableReactions else {
return
}
let state = stateValue.with({ $0 })
guard let updatedMode = state.updatedMode, let updatedAllowedReactions = state.updatedAllowedReactions else {
return
}
let updatedValue: PeerAllowedReactions
switch updatedMode {
case .all:
updatedValue = .all
case .some:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if updatedAllowedReactions == Set(availableReactions.reactions.filter(\.isEnabled).map(\.value)) {
updatedValue = .all
} else {
updatedValue = .limited(Array(updatedAllowedReactions))
}
} else {
updatedValue = .limited(Array(updatedAllowedReactions))
}
case .empty:
updatedValue = .empty
}
if initialAllowedReactions != .known(updatedValue) {
let _ = context.engine.peers.updatePeerReactionSettings(peerId: peerId, reactionSettings: PeerReactionSettings(allowedReactions: updatedValue, maxReactionCount: 11, starsAllowed: nil)).start()
}
})
}
dismissImpl = { [weak controller] in
guard let controller = controller else {
return
}
controller.dismiss()
}
return controller
}
@@ -0,0 +1,257 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import ChatListFilterSettingsHeaderItem
private final class PeerAutoremoveSetupArguments {
let context: AccountContext
let updateValue: (Int32) -> Void
init(context: AccountContext, updateValue: @escaping (Int32) -> Void) {
self.context = context
self.updateValue = updateValue
}
}
private enum PeerAutoremoveSetupSection: Int32 {
case header
case time
}
private enum PeerAutoremoveSetupEntry: ItemListNodeEntry {
case header
case timeHeader(String)
case timeValue(Int32, [Int32])
case timeComment(String)
var section: ItemListSectionId {
switch self {
case .header, .timeHeader, .timeValue, .timeComment:
return PeerAutoremoveSetupSection.time.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .timeHeader:
return 1
case .timeValue:
return 2
case .timeComment:
return 3
}
}
static func ==(lhs: PeerAutoremoveSetupEntry, rhs: PeerAutoremoveSetupEntry) -> Bool {
switch lhs {
case .header:
if case .header = rhs {
return true
} else {
return false
}
case let .timeHeader(lhsText):
if case let .timeHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .timeValue(lhsValue, lhsAvailableValues):
if case let .timeValue(rhsValue, rhsAvailableValues) = rhs, lhsValue == rhsValue, lhsAvailableValues == rhsAvailableValues {
return true
} else {
return false
}
case let .timeComment(lhsText):
if case let .timeComment(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: PeerAutoremoveSetupEntry, rhs: PeerAutoremoveSetupEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PeerAutoremoveSetupArguments
switch self {
case .header:
return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: "", animation: .autoRemove, sectionId: self.section)
case let .timeHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timeValue(value, availableValues):
return PeerRemoveTimeoutItem(presentationData: presentationData, value: value, availableValues: availableValues, enabled: true, sectionId: self.section, updated: { value in
arguments.updateValue(value)
}, tag: nil)
case let .timeComment(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
private struct PeerAutoremoveSetupState: Equatable {
var changedValue: Int32?
var applyingSetting: Bool = false
}
private func peerAutoremoveSetupEntries(peer: EnginePeer?, presentationData: PresentationData, isDebug: Bool, defaultValue: Int32, state: PeerAutoremoveSetupState) -> [PeerAutoremoveSetupEntry] {
var entries: [PeerAutoremoveSetupEntry] = []
let resolvedValue: Int32
resolvedValue = state.changedValue ?? defaultValue
entries.append(.header)
entries.append(.timeHeader(presentationData.strings.AutoremoveSetup_TimeSectionHeader))
var availableValues: [Int32] = [
Int32.max,
24 * 60 * 60,
24 * 60 * 60 * 7,
24 * 60 * 60 * 31,
]
if isDebug {
availableValues[1] = 5
availableValues[2] = 5 * 60
}
entries.append(.timeValue(resolvedValue, availableValues))
if case let .channel(channel) = peer, case .broadcast = channel.info {
entries.append(.timeComment(presentationData.strings.AutoremoveSetup_TimerInfoChannel))
} else {
entries.append(.timeComment(presentationData.strings.AutoremoveSetup_TimerInfoChat))
}
return entries
}
public enum PeerAutoremoveSetupScreenResult {
public struct Updated {
public var value: Int32?
}
case unchanged
case updated(Updated)
}
public func peerAutoremoveSetupScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, completion: @escaping (PeerAutoremoveSetupScreenResult) -> Void = { _ in }) -> ViewController {
let statePromise = ValuePromise(PeerAutoremoveSetupState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeerAutoremoveSetupState())
let updateState: ((PeerAutoremoveSetupState) -> PeerAutoremoveSetupState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let applyDisposable = MetaDisposable()
actionsDisposable.add(applyDisposable)
let arguments = PeerAutoremoveSetupArguments(context: context, updateValue: { value in
updateState { state in
var state = state
state.changedValue = value
return state
}
})
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(presentationData, statePromise.get(), context.account.viewTracker.peerView(peerId))
|> deliverOnMainQueue
|> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, Any)) in
var defaultValue: Int32 = Int32.max
if let cachedData = view.cachedData as? CachedChannelData {
if case let .known(value) = cachedData.autoremoveTimeout {
defaultValue = value?.peerValue ?? Int32.max
}
} else if let cachedData = view.cachedData as? CachedGroupData {
if case let .known(value) = cachedData.autoremoveTimeout {
defaultValue = value?.peerValue ?? Int32.max
}
} else if let cachedData = view.cachedData as? CachedUserData {
if case let .known(value) = cachedData.autoremoveTimeout {
defaultValue = value?.peerValue ?? Int32.max
}
}
let peer = view.peers[view.peerId]
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var rightNavigationButton: ItemListNavigationButton?
if state.applyingSetting {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
var changedValue: Int32?
updateState { state in
var state = state
state.applyingSetting = true
changedValue = state.changedValue
return state
}
var updated = false
if let changedValue = changedValue, changedValue != defaultValue {
updated = true
}
var resolvedValue: Int32? = changedValue ?? defaultValue
if resolvedValue == Int32.max {
resolvedValue = nil
}
if updated {
let signal = context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peerId, timeout: resolvedValue)
|> deliverOnMainQueue
applyDisposable.set((signal
|> deliverOnMainQueue).start(error: { _ in
}, completed: {
dismissImpl?()
if resolvedValue != defaultValue {
completion(.updated(PeerAutoremoveSetupScreenResult.Updated(
value: resolvedValue
)))
} else {
completion(.unchanged)
}
}))
} else {
dismissImpl?()
completion(.unchanged)
}
})
}
let isDebug = context.account.testingEnvironment
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AutoremoveSetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: peerAutoremoveSetupEntries(peer: peer.flatMap(EnginePeer.init), presentationData: presentationData, isDebug: isDebug, defaultValue: defaultValue, state: state), style: .blocks)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,324 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
private func mapTimeoutToSliderValue(_ value: Int32, availableValues: [Int32]) -> CGFloat {
for i in 0 ..< availableValues.count {
if availableValues[i] == Int32.max {
if value == Int32.max {
return CGFloat(i)
}
} else {
if value <= availableValues[i] {
return CGFloat(i)
}
}
}
return CGFloat(availableValues.count - 1)
}
private func mapSliderValueToTimeout(_ value: CGFloat, availableValues: [Int32]) -> Int32 {
let intValue = Int(round(value))
if intValue == 0 {
return Int32.max
} else if intValue >= 0 && intValue < availableValues.count {
return availableValues[intValue]
} else {
return availableValues[availableValues.count - 1]
}
}
class PeerRemoveTimeoutItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let value: Int32
let availableValues: [Int32]
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
let tag: ItemListItemTag?
init(presentationData: ItemListPresentationData, value: Int32, availableValues: [Int32], enabled: Bool = true, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void, tag: ItemListItemTag? = nil) {
self.presentationData = presentationData
self.value = value
self.availableValues = availableValues
self.enabled = enabled
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = PeerRemoveTimeoutItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? PeerRemoveTimeoutItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
class PeerRemoveTimeoutItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let titleNodes: [TextNode]
private let disabledOverlayNode: ASDisplayNode
private var item: PeerRemoveTimeoutItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.disabledOverlayNode = ASDisplayNode()
self.titleNodes = (0 ..< 4).map { _ in
return TextNode()
}
super.init(layerBacked: false)
self.titleNodes.forEach(self.addSubnode)
self.addSubnode(self.disabledOverlayNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = CGFloat(self.titleNodes.count - 1)
sliderView.startValue = 0.0
sliderView.positionsCount = self.titleNodes.count
sliderView.useLinesForPositions = true
sliderView.minimumUndottedValue = 0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.value = mapTimeoutToSliderValue(item.value, availableValues: item.availableValues)
sliderView.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.presentationData.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.enabled ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemDisabledTextColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.presentationData.theme)
let sliderInset: CGFloat = params.leftInset + 16.0
sliderView.frame = CGRect(origin: CGPoint(x: sliderInset, y: 38.0), size: CGSize(width: params.width - sliderInset * 2.0, height: 44.0))
}
self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: PeerRemoveTimeoutItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTitleNodeLayouts = self.titleNodes.map(TextNode.asyncLayout)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.presentationData.theme !== item.presentationData.theme {
themeUpdated = true
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let titleLayouts = zip(0 ..< makeTitleNodeLayouts.count, makeTitleNodeLayouts).map { index, makeLayout -> (TextNodeLayout, () -> TextNode) in
let text: String
if item.availableValues[index] == Int32.max {
text = item.presentationData.strings.AutoremoveSetup_TimerValueNever
} else {
text = timeIntervalString(strings: item.presentationData.strings, value: item.availableValues[index])
}
return makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(13.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let firstTime = strongSelf.item == nil
strongSelf.item = item
strongSelf.layoutParams = params
let leftInset = 16.0 + params.leftInset
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.disabledOverlayNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
strongSelf.disabledOverlayNode.isHidden = item.enabled
strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0))
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let usableWidth = params.width - (leftInset + 7.0) * 2.0
for i in 0 ..< titleLayouts.count {
let textNode = titleLayouts[i].1()
let size = titleLayouts[i].0.size
let nextX: CGFloat
if i == 0 {
nextX = leftInset
} else if i == titleLayouts.count - 1 {
nextX = params.width - leftInset - size.width
} else {
nextX = floor(leftInset + 7.0 + CGFloat(i) * usableWidth / CGFloat(titleLayouts.count - 1) - size.width / 2.0)
}
textNode.frame = CGRect(origin: CGPoint(x: nextX, y: 13.0), size: size)
}
if let sliderView = strongSelf.sliderView {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.trackColor = item.enabled ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemDisabledTextColor
if themeUpdated {
sliderView.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.presentationData.theme.list.itemSwitchColors.frameColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.presentationData.theme)
}
let value: CGFloat
switch item.value {
case 24 * 60 * 60:
value = 0.0
case 7 * 24 * 60 * 60:
value = 1.0
default:
value = 2.0
}
if firstTime {
sliderView.value = value
}
sliderView.frame = CGRect(origin: CGPoint(x: leftInset, y: 38.0), size: CGSize(width: params.width - leftInset * 2.0, height: 44.0))
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView, let item = self.item else {
return
}
self.item?.updated(mapSliderValueToTimeout(sliderView.value, availableValues: item.availableValues))
}
}
@@ -0,0 +1,127 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import UIKitRuntimeUtils
final class PeerBanTimeoutController: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, currentValue: Int32, applyValue: @escaping (Int32?) -> Void) {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self._ready.set(.single(true))
self.presentationDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
var updatedValue = currentValue
var items: [ActionSheetItem] = []
items.append(PeerBanTimeoutActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in
updatedValue = value
}))
items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in
self?.dismissAnimated()
applyValue(updatedValue)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class PeerBanTimeoutActionSheetItem: ActionSheetItem {
let strings: PresentationStrings
let currentValue: Int32
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.currentValue = /*roundDateToDays(*/currentValue/*)*/
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return PeerBanTimeoutActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class PeerBanTimeoutActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let valueChanged: (Int32) -> Void
private let pickerView: UIDatePicker
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .dateAndTime
self.pickerView.date = Date(timeIntervalSince1970: Double(/*roundDateToDays(*/currentValue/*)*/))
self.pickerView.locale = localeWithStrings(strings)
self.pickerView.minimumDate = Date()
self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1))
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 216.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func datePickerUpdated() {
self.valueChanged(/*roundDateToDays(*/Int32(self.pickerView.date.timeIntervalSince1970)/*)*/)
}
}
@@ -0,0 +1,144 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import TelegramStringFormatting
private struct PhoneLabelArguments {
let selectLabel: (String) -> Void
let complete: () -> Void
let cancel: () -> Void
}
private struct PhoneLabelState: Equatable {
var currentLabel: String
}
private enum PhoneLabelSection: Int32 {
case labels
}
private enum PhoneLabelEntryId: Hashable {
case label(String)
}
private enum PhoneLabelEntry: ItemListNodeEntry {
case label(Int, PresentationTheme, String, String, Bool)
var section: ItemListSectionId {
switch self {
case .label:
return PhoneLabelSection.labels.rawValue
}
}
var stableId: PhoneLabelEntryId {
switch self {
case let .label(_, _, label, _, _):
return .label(label)
}
}
var index: Int {
switch self {
case let .label(index, _, _, _, _):
return index
}
}
static func <(lhs: PhoneLabelEntry, rhs: PhoneLabelEntry) -> Bool {
return lhs.index < rhs.index
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PhoneLabelArguments
switch self {
case let .label(_, _, value, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectLabel(value)
})
}
}
}
private func phoneLabelEntries(presentationData: PresentationData, state: PhoneLabelState) -> [PhoneLabelEntry] {
var entries: [PhoneLabelEntry] = []
let labels: [String] = [
"_$!<Work>!$_",
"X-iPhone",
"_$!<Mobile>!$_",
"_$!<Main>!$_",
"_$!<Pager>!$_",
"_$!<Other>!$_",
]
for label in labels {
entries.append(.label(entries.count, presentationData.theme, label, localizedPhoneNumberLabel(label: label, strings: presentationData.strings), state.currentLabel == label))
}
return entries
}
public func phoneLabelController(context: AccountContext, currentLabel: String, completion: @escaping (String) -> Void) -> ViewController {
let statePromise = ValuePromise(PhoneLabelState(currentLabel: currentLabel))
let stateValue = Atomic(value: PhoneLabelState(currentLabel: currentLabel))
let updateState: ((PhoneLabelState) -> PhoneLabelState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var completeImpl: (() -> Void)?
var cancelImpl: (() -> Void)?
let arguments = PhoneLabelArguments(selectLabel: { label in
updateState { state in
var state = state
state.currentLabel = label
return state
}
}, complete: {
completeImpl?()
}, cancel: {
cancelImpl?()
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
arguments.cancel()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
arguments.complete()
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PhoneLabel_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: phoneLabelEntries(presentationData: presentationData, state: state), style: .blocks)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal
|> afterDisposed {
})
controller.navigationPresentation = .modal
controller.enableInteractiveDismiss = true
completeImpl = { [weak controller] in
let currentLabel = stateValue.with({ $0 }).currentLabel
completion(currentLabel)
controller?.dismiss()
}
cancelImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}
@@ -0,0 +1,51 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import AccountContext
public final class SecretChatKeyController: ViewController {
private var controllerNode: SecretChatKeyControllerNode {
return self.displayNode as! SecretChatKeyControllerNode
}
private let context: AccountContext
private let fingerprint: SecretChatKeyFingerprint
private let peer: EnginePeer
private var presentationData: PresentationData
public init(context: AccountContext, fingerprint: SecretChatKeyFingerprint, peer: EnginePeer) {
self.context = context
self.fingerprint = fingerprint
self.peer = peer
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.navigationPresentation = .modal
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.title = self.presentationData.strings.EncryptionKey_Title
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = SecretChatKeyControllerNode(context: self.context, presentationData: self.presentationData, fingerprint: self.fingerprint, peer: self.peer, getNavigationController: { [weak self] in
return self?.navigationController as? NavigationController
})
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
@@ -0,0 +1,167 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TextFormat
import AccountContext
import EncryptionKeyVisualization
import LocalizedPeerData
private func processHexString(_ string: String) -> String {
var result = ""
var i = 0
for c in string {
if i % 2 == 0 && i != 0 {
result.append(" ")
}
if i % 8 == 0 && i != 0 {
result.append(" ")
}
result.append(c)
i += 1
}
return result
}
final class SecretChatKeyControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let fingerprint: SecretChatKeyFingerprint
private let peer: EnginePeer
private let getNavigationController: () -> NavigationController?
private let scrollNode: ASScrollNode
private let imageNode: ASImageNode
private let keyTextNode: TextNode
private let infoNode: TextNode
private var validImageSize: CGSize?
init(context: AccountContext, presentationData: PresentationData, fingerprint: SecretChatKeyFingerprint, peer: EnginePeer, getNavigationController: @escaping () -> NavigationController?) {
self.context = context
self.presentationData = presentationData
self.fingerprint = fingerprint
self.peer = peer
self.getNavigationController = getNavigationController
self.scrollNode = ASScrollNode()
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.keyTextNode = TextNode()
self.keyTextNode.isUserInteractionEnabled = false
self.keyTextNode.displaysAsynchronously = false
self.infoNode = TextNode()
self.infoNode.displaysAsynchronously = false
super.init()
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.imageNode)
self.scrollNode.addSubnode(self.keyTextNode)
self.scrollNode.addSubnode(self.infoNode)
}
override func didLoad() {
super.didLoad()
self.infoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.infoTap(_:))))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top))
let sideInset: CGFloat = 10.0
var imageSize = CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.width - sideInset * 2.0)
if imageSize.height > layout.size.height - insets.top - sideInset * 2.0 - 100.0 {
let side = layout.size.height - insets.top - sideInset * 2.0 - 100.0
imageSize = CGSize(width: side, height: side)
}
if imageSize.height > 512.0 {
imageSize = CGSize(width: 512.0, height: 512.0)
}
if self.validImageSize != imageSize {
self.validImageSize = imageSize
self.imageNode.image = secretChatKeyImage(self.fingerprint, size: imageSize)
}
let makeKeyTextLayout = TextNode.asyncLayout(self.keyTextNode)
let makeInfoLayout = TextNode.asyncLayout(self.infoNode)
let keySignatureData = self.fingerprint.sha1.data()
let additionalSignature = self.fingerprint.sha256.data()
var data = Data()
data.append(keySignatureData)
data.append(additionalSignature)
let s1: String = (data.subdata(in: 0 ..< 8) as NSData).stringByEncodingInHex()
let s2: String = (data.subdata(in: 8 ..< 16) as NSData).stringByEncodingInHex()
let s3: String = (additionalSignature.subdata(in: 0 ..< 8) as NSData).stringByEncodingInHex()
let s4: String = (additionalSignature.subdata(in : 8 ..< 16) as NSData).stringByEncodingInHex()
let text: String = "\(processHexString(s1))\n\(processHexString(s2))\n\(processHexString(s3))\n\(processHexString(s4))"
let (keyTextLayout, keyTextApply) = makeKeyTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.semiboldMonospace(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let infoString = self.presentationData.strings.EncryptionKey_Description(self.peer.compactDisplayTitle, self.peer.compactDisplayTitle)
let infoText = NSMutableAttributedString(string: infoString.string, attributes: [.font: Font.regular(14.0), .foregroundColor: self.presentationData.theme.list.itemPrimaryTextColor])
for range in infoString.ranges {
infoText.addAttributes([.font: Font.semibold(14.0)], range: range.range)
}
let linkRange = (infoString.string as NSString).range(of: "telegram.org")
if linkRange.location != NSNotFound {
infoText.addAttributes([.foregroundColor: self.presentationData.theme.list.itemAccentColor, NSAttributedString.Key(rawValue: TelegramTextAttributes.URL): "https://telegram.org/faq#secret-chats"], range: linkRange)
}
let (infoLayout, infoApply) = makeInfoLayout(TextNodeLayoutArguments(attributedString: infoText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let _ = keyTextApply()
let _ = infoApply()
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 10.0
let contentHeight = imageSize.height + imageSpacing + keyTextLayout.size.height + textSpacing + infoLayout.size.height
let contentOrigin = sideInset + max(0, floor((layout.size.height - insets.top - sideInset * 2.0 - contentHeight) / 2.0))
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight + sideInset * 2.0)
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: contentOrigin), size: imageSize)
transition.updateFrame(node: self.imageNode, frame: imageFrame)
let keyTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - keyTextLayout.size.width) / 2.0), y: imageFrame.maxY + imageSpacing), size: keyTextLayout.size)
transition.updateFrame(node: self.keyTextNode, frame: keyTextFrame)
let infoFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - infoLayout.size.width) / 2.0), y: keyTextFrame.maxY + textSpacing), size: infoLayout.size)
transition.updateFrame(node: self.infoNode, frame: infoFrame)
}
@objc func infoTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: recognizer.view)
if let attributes = self.infoNode.attributesAtPoint(point)?.1 {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: { [weak self] in
self?.view.endEditing(true)
})
}
}
}
}
}
@@ -0,0 +1,78 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import TextFormat
import OverlayStatusController
import TelegramStringFormatting
import AccountContext
import ShareController
import AlertUI
import PresentationDataUtils
import TelegramNotices
import GalleryUI
import ItemListAvatarAndNameInfoItem
import PeerAvatarGalleryUI
import NotificationMuteSettingsUI
import NotificationSoundSelectionUI
import Markdown
import LocalizedPeerData
import PhoneNumberFormat
import TelegramIntents
private func getUserPeer(engine: TelegramEngine, peerId: EnginePeer.Id) -> Signal<(EnginePeer?, EnginePeer.StatusSettings?), NoError> {
return engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> mapToSignal { peer -> Signal<EnginePeer?, NoError> in
if case let .secretChat(secretChat) = peer {
return engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: secretChat.regularPeerId))
} else {
return .single(peer)
}
}
|> mapToSignal { peer -> Signal<(EnginePeer?, EnginePeer.StatusSettings?), NoError> in
guard let peer = peer else {
return .single((nil, nil))
}
return engine.data.get(TelegramEngine.EngineData.Item.Peer.StatusSettings(id: peer.id))
|> map { statusSettings -> (EnginePeer?, EnginePeer.StatusSettings?) in
return (peer, statusSettings)
}
}
}
public func openAddPersonContactImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void, completion: @escaping () -> Void = {}) {
let _ = (getUserPeer(engine: context.engine, peerId: peerId)
|> deliverOnMainQueue).start(next: { peer, statusSettings in
guard let peer, case let .user(user) = peer else {
return
}
var shareViaException = false
if let statusSettings = statusSettings {
shareViaException = statusSettings.contains(.addExceptionWhenAddingContact)
}
let controller = context.sharedContext.makeNewContactScreen(
context: context,
peer: peer,
phoneNumber: user.phone,
shareViaException: shareViaException,
completion: { peer, _, _ in
if let peer {
completion()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.AddContact_StatusSuccess(peer.compactDisplayTitle).string, true)), nil)
}
}
)
pushController(controller)
})
}
@@ -0,0 +1,232 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
class UserInfoEditingPhoneActionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let title: String
let sectionId: ItemListSectionId
let action: () -> Void
init(presentationData: ItemListPresentationData, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, tag: Any? = nil) {
self.presentationData = presentationData
self.title = title
self.sectionId = sectionId
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = UserInfoEditingPhoneActionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? UserInfoEditingPhoneActionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
class UserInfoEditingPhoneActionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private var item: UserInfoEditingPhoneActionItem?
var tag: Any? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
}
func asyncLayout() -> (_ item: UserInfoEditingPhoneActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let textColor = item.presentationData.theme.list.itemAccentColor
let iconImage = PresentationResourcesItemList.addPhoneIcon(item.presentationData.theme)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 22.0 + titleLayout.size.height)
insets = itemListNeighborsPlainInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
strongSelf.iconNode.image = iconImage
let _ = titleApply()
let leftInset: CGFloat
leftInset = 16.0 + params.leftInset
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
if let iconImage = iconImage {
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - iconImage.size.height) / 2.0)), size: iconImage.size)
}
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 30.0, y: 12.0), size: titleLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,354 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import SinglePhoneInputNode
import AppBundle
private func generateClearIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
}
struct UserInfoEditingPhoneItemEditing {
let editable: Bool
let hasActiveRevealControls: Bool
}
class UserInfoEditingPhoneItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let id: Int64
let label: String
let value: String
let editing: UserInfoEditingPhoneItemEditing
let sectionId: ItemListSectionId
let setPhoneIdWithRevealedOptions: (Int64?, Int64?) -> Void
let updated: (String) -> Void
let selectLabel: (() -> Void)?
let delete: () -> Void
let tag: ItemListItemTag?
init(presentationData: ItemListPresentationData, id: Int64, label: String, value: String, editing: UserInfoEditingPhoneItemEditing, sectionId: ItemListSectionId, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, updated: @escaping (String) -> Void, selectLabel: (() -> Void)?, delete: @escaping () -> Void, tag: ItemListItemTag?) {
self.presentationData = presentationData
self.id = id
self.label = label
self.value = value
self.editing = editing
self.sectionId = sectionId
self.setPhoneIdWithRevealedOptions = setPhoneIdWithRevealedOptions
self.updated = updated
self.selectLabel = selectLabel
self.delete = delete
self.tag = tag
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = UserInfoEditingPhoneItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? UserInfoEditingPhoneItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = false
}
class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, ItemListItemFocusableNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let labelNode: TextNode
private let labelButtonNode: HighlightTrackingButtonNode
private let editableControlNode: ItemListEditableControlNode
private let labelSeparatorNode: ASDisplayNode
private let phoneNode: SinglePhoneInputNode
private let clearButton: HighlightableButtonNode
private var item: UserInfoEditingPhoneItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.editableControlNode = ItemListEditableControlNode()
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .left
self.labelNode.contentsScale = UIScreen.main.scale
self.labelButtonNode = HighlightTrackingButtonNode()
self.labelSeparatorNode = ASDisplayNode()
self.labelSeparatorNode.isLayerBacked = true
self.phoneNode = SinglePhoneInputNode(fontSize: 17.0)
self.clearButton = HighlightableButtonNode()
self.clearButton.imageNode.displaysAsynchronously = false
self.clearButton.imageNode.displayWithoutProcessing = true
self.clearButton.displaysAsynchronously = false
self.clearButton.isHidden = true
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.editableControlNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.labelButtonNode)
self.addSubnode(self.labelSeparatorNode)
self.addSubnode(self.phoneNode)
self.addSubnode(self.clearButton)
self.labelButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
strongSelf.labelNode.alpha = 0.4
} else {
strongSelf.labelNode.alpha = 1.0
strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.labelButtonNode.addTarget(self, action: #selector(self.labelPressed), forControlEvents: .touchUpInside)
self.editableControlNode.tapped = { [weak self] in
if let strongSelf = self {
strongSelf.setRevealOptionsOpened(true, animated: true)
strongSelf.revealOptionsInteractivelyOpened()
}
}
self.phoneNode.numberUpdated = { [weak self] number in
self?.item?.updated(number)
self?.updateClearButtonVisibility()
}
self.phoneNode.beginEditing = { [weak self] in
self?.updateClearButtonVisibility()
}
self.phoneNode.endEditing = { [weak self] in
self?.updateClearButtonVisibility()
}
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
}
override func didLoad() {
super.didLoad()
if let item = self.item {
self.phoneNode.numberField?.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor
self.phoneNode.numberField?.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.phoneNode.numberField?.textField.tintColor = item.presentationData.theme.list.itemAccentColor
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
self.phoneNode.numberField?.textField.font = titleFont
}
}
func asyncLayout() -> (_ item: UserInfoEditingPhoneItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
return { item, params, neighbors in
let titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let controlSizeAndApply = editableControlLayout(item.presentationData.theme, false)
let textColor = item.presentationData.theme.list.itemAccentColor
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: 22.0 + labelLayout.size.height)
insets = itemListNeighborsPlainInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let updatedTheme = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.labelSeparatorNode.backgroundColor = itemSeparatorColor
strongSelf.phoneNode.numberField?.textField.textColor = updatedTheme.list.itemPrimaryTextColor
strongSelf.phoneNode.numberField?.textField.keyboardAppearance = updatedTheme.rootController.keyboardColor.keyboardAppearance
strongSelf.phoneNode.numberField?.textField.tintColor = item.presentationData.theme.list.itemAccentColor
strongSelf.phoneNode.numberField?.textField.font = titleFont
strongSelf.clearButton.setImage(generateClearIcon(color: updatedTheme.list.inputClearButtonColor), for: [])
}
let revealOffset = strongSelf.revealOffset
let _ = labelApply()
let leftInset: CGFloat
leftInset = 16.0 + params.leftInset
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
let _ = controlSizeAndApply.1(layout.contentSize.height)
let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + 4.0 + revealOffset, y: 0.0), size: CGSize(width: controlSizeAndApply.0, height: layout.contentSize.height))
strongSelf.editableControlNode.frame = editableControlFrame
let labelFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset + 30.0, y: 12.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
strongSelf.labelButtonNode.frame = labelFrame
strongSelf.labelButtonNode.isUserInteractionEnabled = item.selectLabel != nil
strongSelf.labelSeparatorNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + 8.0, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.contentSize.height))
let phoneX = labelFrame.maxX + 16.0
let phoneFrame = CGRect(origin: CGPoint(x: phoneX, y: 0.0), size: CGSize(width: max(1.0, params.width - params.rightInset - phoneX), height: layout.contentSize.height))
strongSelf.phoneNode.frame = phoneFrame
strongSelf.phoneNode.updateLayout(size: phoneFrame.size)
strongSelf.phoneNode.number = item.value
if let image = strongSelf.clearButton.image(for: []) {
strongSelf.clearButton.frame = CGRect(origin: CGPoint(x: phoneFrame.maxX - image.size.width - 23.0, y: phoneFrame.minY + floor((phoneFrame.size.height - image.size.height) / 2.0) - 1.0 + UIScreenPixel), size: image.size)
}
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
}
})
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
guard let params = self.layoutParams else {
return
}
let revealOffset = offset
let leftInset = 16.0 + params.leftInset
var controlFrame = self.editableControlNode.frame
controlFrame.origin.x = params.leftInset + 4.0 + revealOffset
transition.updateFrame(node: self.editableControlNode, frame: controlFrame)
var labelFrame = self.labelNode.frame
labelFrame.origin.x = revealOffset + leftInset + 30.0
transition.updateFrame(node: self.labelNode, frame: labelFrame)
var labelSeparatorFrame = self.labelSeparatorNode.frame
labelSeparatorFrame.origin.x = labelFrame.maxX + 8.0
transition.updateFrame(node: self.labelSeparatorNode, frame: labelSeparatorFrame)
var phoneFrame = self.phoneNode.frame
phoneFrame.origin.x = labelFrame.maxX + 16.0
transition.updateFrame(node: self.phoneNode, frame: phoneFrame)
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
self.item?.delete()
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func labelPressed() {
self.item?.selectLabel?()
}
@objc func clearPressed() {
self.phoneNode.numberField?.textField.text = "+"
self.updateClearButtonVisibility()
}
private func updateClearButtonVisibility() {
let text = self.phoneNode.numberField?.textField.text ?? ""
let isEmpty = text.isEmpty || text == "+"
self.clearButton.isHidden = isEmpty || !(self.phoneNode.numberField?.textField.isFirstResponder ?? false)
}
func focus() {
self.phoneNode.numberField?.becomeFirstResponder()
}
func selectAll() {
self.phoneNode.numberField?.textField.selectAll(nil)
}
}