Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-01-17 10:18:10 +03:00
parent fadd3abea6
commit fa125d0192
124 changed files with 12748 additions and 5704 deletions

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IntentPhrases</key>
<array>
<dict>
<key>IntentName</key>
<string>INSendMessageIntent</string>
<key>IntentExamples</key>
<array>
<string>Send a Telegram message to Alex saying I&apos;ll be there in 10 minutes</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,12 @@
/* Localized versions of Info.plist keys */
"NSContactsUsageDescription" = "Telegram будзе запампоўваць вашы кантакты на свае моцна абароненыя і зашыфраваныя воблачныя серверы, каб вы маглі кантактаваць са сваімі сябрамі з любой вашай прылады.";
"NSLocationWhenInUseUsageDescription" = "Калі вы адпраўляе месцазнаходжанне сваім сябрам, Telegram патрэбны доступ да сэрвісаў геалакацыі, каб размясціць вас на карце.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі.";
"NSLocationAlwaysUsageDescription" = "Калі вы трансліруеце ваша месцазнаходжанне сябрам у чаце, Telegram патрэбны доступ у фонавым рэжыме да вашага месцазнаходжання, каб абнаўляць яго падчас трансляцыі. Гэта таксама неабходна для таго, каб адпраўляць месцазнаходжанне праз Apple Watch.";
"NSCameraUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі рабіць і адпраўляць фота і відэа, а таксама відэавыклікі.";
"NSPhotoLibraryUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі абагульваць фота і відэа са сваёй галерэі.";
"NSPhotoLibraryAddUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі захоўваць фота і відэа ў сваю галерэю.";
"NSMicrophoneUsageDescription" = "Нам неабходны гэты дазвол, каб вы маглі запісваць і абагульваць галасавыя паведамленні і відэа з гукам.";
"NSSiriUsageDescription" = "Вы можаце адпраўляць паведамленні праз Siri.";
"NSFaceIDUsageDescription" = "Вы можаце карыстацца Face ID для разблакіравання праграмы.";

View file

@ -5816,3 +5816,76 @@ Sorry for the inconvenience.";
"PeerInfo.ButtonVoiceChat" = "Voice Chat";
"VoiceChat.OpenChat" = "Open Chat";
"GroupInfo.InviteLinks" = "Invite Links";
"InviteLink.Title" = "Invite Links";
"InviteLink.PermanentLink" = "Permanent Link";
"InviteLink.Share" = "Share Link";
"InviteLink.PeopleJoinedNone" = "no one joined yet";
"InviteLink.PeopleJoined_1" = "%@ people joined";
"InviteLink.PeopleJoined_2" = "%@ people joined";
"InviteLink.PeopleJoined_3_10" = "%@ people joined";
"InviteLink.PeopleJoined_many" = "%@ people joined";
"InviteLink.PeopleJoined_any" = "%@ people joined";
"InviteLink.CreatePrivateLinkHelp" = "Anyone who has Telegram installed will be able to join your group by following this link.";
"InviteLink.Manage" = "Manage Invite Links";
"InviteLink.PeopleJoinedShortNoneExpired" = "no one joined";
"InviteLink.PeopleJoinedShortNone" = "no one joined yet";
"InviteLink.PeopleJoinedShort_1" = "%@ joined";
"InviteLink.PeopleJoinedShort_2" = "%@ joined";
"InviteLink.PeopleJoinedShort_3_10" = "%@ joined";
"InviteLink.PeopleJoinedShort_many" = "%@ joined";
"InviteLink.PeopleJoinedShort_any" = "%@ joined";
"InviteLink.Expired" = "expired";
"InviteLink.UsageLimitReached" = "limit reached";
"InviteLink.Revoked" = "revoked";
"InviteLink.AdditionalLinks" = "Additional Links";
"InviteLink.Create" = "Create a New Link";
"InviteLink.CreateInfo" = "You can create additional invite links that have limited time or number of usages.";
"InviteLink.RevokedLinks" = "Revoked Links";
"InviteLink.DeleteAllRevokedLinks" = "Delete All Revoked Links";
"InviteLink.ContextCopy" = "Copy";
"InviteLink.ContextEdit" = "Edit";
"InviteLink.ContextGetQRCode" = "Get QR Code";
"InviteLink.ContextShare" = "Share";
"InviteLink.ContextRevoke" = "Revoke";
"InviteLink.ContextDelete" = "Delete";
"InviteLink.Create.Title" = "New Link";
"InviteLink.Create.EditTitle" = "Edit Link";
"InviteLink.Create.TimeLimit" = "Limit By Time Period";
"InviteLink.Create.TimeLimitExpiryDate" = "Expiry Date";
"InviteLink.Create.TimeLimitExpiryDateNever" = "Never";
"InviteLink.Create.TimeLimitExpiryTime" = "Time";
"InviteLink.Create.TimeLimitInfo" = "You can make the link expire after a certain time.";
"InviteLink.Create.TimeLimitNoLimit" = "No Limit";
"InviteLink.Create.UsersLimit" = "Limit By Number Of Users";
"InviteLink.Create.UsersLimitNumberOfUsers" = "Number of Uses";
"InviteLink.Create.UsersLimitNumberOfUsersUnlimited" = "Unlimited";
"InviteLink.Create.UsersLimitInfo" = "You can make the link expire after it has been used for a certain number of times.";
"InviteLink.Create.UsersLimitNoLimit" = "No Limit";
"InviteLink.Create.Revoke" = "Revoke Link";
"InviteLink.QRCode.Title" = "Invite by QR Code";
"InviteLink.QRCode.Info" = "Everyone on Telegram can scan this code to join your group.";
"InviteLink.QRCode.Share" = "Share QR Code";
"InviteLink.InviteLink" = "Invite Link";
"InviteLink.CreatedBy" = "Link Created By";
"InviteLink.DeleteAllRevokedLinksAlert.Text" = "This will delete all revoked links";
"InviteLink.DeleteAllRevokedLinksAlert.Action" = "Delete";
"Conversation.ChecksTooltip.Delivered" = "Delivered";
"Conversation.ChecksTooltip.Read" = "Read";
"DialogList.MultipleTypingPair" = "%@ and %@ are typing";

View file

@ -2,11 +2,11 @@
"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства.";
"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. ";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями.";
"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch.";
"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео.";
"NSPhotoLibraryUsageDescription" = "Это необходимо, чтобы вы могли делиться фото и видео из библиотеки устройства.";
"NSPhotoLibraryAddUsageDescription" = "Это необходимо, чтобы вы могли сохранять фото и видео в библиотеку устройства.";
"NSMicrophoneUsageDescription" = "Это необходимо, чтобы вы могли делиться голосовыми сообщениями и видео со звуком.";
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений";
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений.";
"NSFaceIDUsageDescription" = "Вы можете разблокировать приложение с помощью Face ID.";

View file

@ -1970,6 +1970,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let _ = (signal
|> deliverOnMainQueue).start()
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
for peerId in peerIds {
state.selectedPeerIds.remove(peerId)
}
return state
})
return true
} else if value == .undo {
strongSelf.chatListDisplayNode.containerNode.currentItemNode.setCurrentRemovingPeerId(peerIds.first!)
@ -2524,6 +2533,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
deleteSendMessageIntents(peerId: peerId)
})
strongSelf.chatListDisplayNode.containerNode.updateState({ state in
var state = state
state.selectedPeerIds.remove(peerId)
return state
})
completion()
return true
} else if value == .undo {

View file

@ -684,7 +684,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers)
|> map { actions -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
if let linkForCopying = linkForCopying {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
@ -948,17 +947,25 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
})
}) |> deliverOnMainQueue).start(completed: {
if let strongSelf = self {
// strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
let controller = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))
controller.purposefulAction = { [weak self] in
self?.cancel?()
}
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
if let navigationController = strongSelf.navigationController, let peerSelectionControllerIndex = navigationController.viewControllers.firstIndex(where: { $0 is PeerSelectionController }) {
var viewControllers = navigationController.viewControllers
viewControllers.insert(controller, at: peerSelectionControllerIndex)
navigationController.setViewControllers(viewControllers, animated: false)
Queue.mainQueue().after(0.2) {
peerSelectionController?.dismiss()
}
})
} else {
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
}
})
}
strongSelf.updateState { state in
return state.withUpdatedSelectedMessageIds(nil)

View file

@ -129,9 +129,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private var pendingPanes: [ChatListSearchPaneKey: ChatListSearchPendingPane] = [:]
private var transitionFraction: CGFloat = 0.0
var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)?
var requestExpandTabs: (() -> Bool)?

View file

@ -1354,7 +1354,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _):
if !displayAsMessage {
if let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
if let peer = renderedPeer.chatMainPeer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
let updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0)
let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp)
if case .online = relativeStatus {

View file

@ -127,7 +127,12 @@ final class ChatListInputActivitiesNode: ASDisplayNode {
let string: NSAttributedString
if activities.count > 1 {
let peerTitle = activities[0].0.compactDisplayTitle
string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).0).0, font: textFont, textColor: color)
if activities.count == 2 {
let secondPeerTitle = activities[1].0.compactDisplayTitle
string = NSAttributedString(string: strings.DialogList_MultipleTypingPair(peerTitle, secondPeerTitle).0, font: textFont, textColor: color)
} else {
string = NSAttributedString(string: strings.DialogList_MultipleTyping(peerTitle, strings.DialogList_MultipleTypingSuffix(activities.count - 1).0).0, font: textFont, textColor: color)
}
} else {
string = NSAttributedString(string: strings.DialogList_MultipleTypingSuffix(activities.count).0, font: textFont, textColor: color)
}

View file

@ -58,15 +58,21 @@ private func loadCountryCodes() -> [(String, Int)] {
private let countryCodes: [(String, Int)] = loadCountryCodes()
func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, Int)] {
func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] {
let locale = localeWithStrings(strings)
var result: [((String, String), String, Int)] = []
var result: [((String, String), String, [Int])] = []
for country in AuthorizationSequenceCountrySelectionController.countries() {
if country.hidden {
continue
}
if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id), let codeValue = country.countryCodes.first?.code, let code = Int(codeValue) {
result.append(((englishCountryName, countryName), country.id, code))
if let englishCountryName = usEnglishLocale.localizedString(forRegionCode: country.id), let countryName = locale.localizedString(forRegionCode: country.id) {
var codes: [Int] = []
for codeValue in country.countryCodes {
if let code = Int(codeValue.code) {
codes.append(code)
}
}
result.append(((englishCountryName, countryName), country.id, codes))
} else {
assertionFailure()
}
@ -128,7 +134,7 @@ private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey
return false
}
private func searchCountries(items: [((String, String), String, Int)], query: String) -> [((String, String), String, Int)] {
private func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] {
let queryTokens = stringTokens(query.lowercased())
var result: [((String, String), String, Int)] = []
@ -136,7 +142,9 @@ private func searchCountries(items: [((String, String), String, Int)], query: St
let string = "\(item.0) \(item.1)"
let tokens = stringTokens(string)
if matchStringTokens(tokens, with: queryTokens) {
result.append(item)
for code in item.2 {
result.append((item.0, item.1, code))
}
}
}
@ -158,7 +166,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode,
private let sectionTitles: [String]
private var searchResults: [((String, String), String, Int)] = []
private let countryNamesAndCodes: [((String, String), String, Int)]
private let countryNamesAndCodes: [((String, String), String, [Int])]
init(theme: PresentationTheme, strings: PresentationStrings, displayCodes: Bool, itemSelected: @escaping (((String, String), String, Int)) -> Void) {
self.theme = theme
@ -181,14 +189,16 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode,
self.countryNamesAndCodes = countryNamesAndCodes
var sections: [(String, [((String, String), String, Int)])] = []
for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in
for (names, id, codes) in countryNamesAndCodes.sorted(by: { lhs, rhs in
return lhs.0.1 < rhs.0.1
}) {
let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased()
if sections.isEmpty || sections[sections.count - 1].0 != title {
sections.append((title, []))
}
sections[sections.count - 1].1.append((names, id, code))
for code in codes {
sections[sections.count - 1].1.append((names, id, code))
}
}
self.sections = sections
self.sectionTitles = sections.map { $0.0 }

View file

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "DatePickerNode",
module_name = "DatePickerNode",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,425 @@
//import Foundation
//import Display
//import UIKit
//import AsyncDisplayKit
//import TelegramPresentationData
//
//private let textFont = Font.regular(13.0)
//private let selectedTextFont = Font.bold(13.0)
//
//public final class DatePickerTheme: Equatable {
// public let backgroundColor: UIColor
// public let textColor: UIColor
// public let secondaryTextColor: UIColor
// public let accentColor: UIColor
// public let selectionColor: UIColor
// public let selectedCurrentTextColor: UIColor
// public let secondarySelectionColor: UIColor
//
// public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, selectionColor: UIColor, selectedCurrentTextColor: UIColor, secondarySelectionColor: UIColor) {
// self.backgroundColor = backgroundColor
// self.textColor = textColor
// self.secondaryTextColor = secondaryTextColor
// self.accentColor = accentColor
// self.selectionColor = selectionColor
// self.selectedCurrentTextColor = selectedCurrentTextColor
// self.secondarySelectionColor = secondarySelectionColor
// }
//
// public static func ==(lhs: DatePickerTheme, rhs: DatePickerTheme) -> Bool {
// if lhs.backgroundColor != rhs.backgroundColor {
// return false
// }
// if lhs.textColor != rhs.textColor {
// return false
// }
// if lhs.secondaryTextColor != rhs.secondaryTextColor {
// return false
// }
// if lhs.accentColor != rhs.accentColor {
// return false
// }
// if lhs.selectionColor != rhs.selectionColor {
// return false
// }
// if lhs.selectedCurrentTextColor != rhs.selectedCurrentTextColor {
// return false
// }
// if lhs.secondarySelectionColor != rhs.secondarySelectionColor {
// return false
// }
// return true
// }
//}
//
//public extension DatePickerTheme {
// convenience init(theme: PresentationTheme) {
// self.init(backgroundColor: theme.rootController.navigationBar.segmentedBackgroundColor, foregroundColor: theme.rootController.navigationBar.segmentedForegroundColor, shadowColor: .black, textColor: theme.rootController.navigationBar.segmentedTextColor, dividerColor: theme.rootController.navigationBar.segmentedDividerColor)
// }
//}
//
//private class SegmentedControlItemNode: HighlightTrackingButtonNode {
//}
//
//private let telegramReleaseDate = Date(timeIntervalSince1970: 1376438400.0)
//
//public final class DatePickerNode: ASDisplayNode, UIGestureRecognizerDelegate {
// private var theme: DatePickerTheme
// private var _items: [SegmentedControlItem]
// private var _selectedIndex: Int = 0
//
// private var validLayout: SegmentedControlLayout?
//
// private let selectionNode: ASImageNode
// private var itemNodes: [SegmentedControlItemNode]
// private var dividerNodes: [ASDisplayNode]
//
// private var gestureRecognizer: UIPanGestureRecognizer?
// private var gestureSelectedIndex: Int?
//
// public var maximumDate: Date? {
// didSet {
//
// }
// }
// public var minimumDate: Date = telegramReleaseDate {
// didSet {
//
// }
// }
// public var date: Date = Date() {
// didSet {
//
// }
// }
//
//
// public var items: [SegmentedControlItem] {
// get {
// return self._items
// }
// set {
// let previousItems = self._items
// self._items = newValue
// guard previousItems != newValue else {
// return
// }
//
// self.itemNodes.forEach { $0.removeFromSupernode() }
// self.itemNodes = self._items.map { item in
// let itemNode = SegmentedControlItemNode()
// itemNode.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
// itemNode.titleNode.maximumNumberOfLines = 1
// itemNode.titleNode.truncationMode = .byTruncatingTail
// itemNode.setTitle(item.title, with: textFont, with: self.theme.textColor, for: .normal)
// itemNode.setTitle(item.title, with: selectedTextFont, with: self.theme.textColor, for: .selected)
// itemNode.setTitle(item.title, with: selectedTextFont, with: self.theme.textColor, for: [.selected, .highlighted])
// return itemNode
// }
// self.setupButtons()
// self.itemNodes.forEach(self.addSubnode(_:))
//
// let dividersCount = self._items.count > 2 ? self._items.count - 1 : 0
// if self.dividerNodes.count != dividersCount {
// self.dividerNodes.forEach { $0.removeFromSupernode() }
// self.dividerNodes = (0 ..< dividersCount).map { _ in ASDisplayNode() }
// }
//
// if let layout = self.validLayout {
// let _ = self.updateLayout(layout, transition: .immediate)
// }
// }
// }
//
// public var selectedIndex: Int {
// get {
// return self._selectedIndex
// }
// set {
// guard newValue != self._selectedIndex else {
// return
// }
// self._selectedIndex = newValue
// if let layout = self.validLayout {
// let _ = self.updateLayout(layout, transition: .immediate)
// }
// }
// }
//
// public func setSelectedIndex(_ index: Int, animated: Bool) {
// guard index != self._selectedIndex else {
// return
// }
// self._selectedIndex = index
// if let layout = self.validLayout {
// let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
// }
// }
//
// public var selectedIndexChanged: (Int) -> Void = { _ in }
// public var selectedIndexShouldChange: (Int, @escaping (Bool) -> Void) -> Void = { _, f in
// f(true)
// }
//
// public init(theme: SegmentedControlTheme, items: [SegmentedControlItem], selectedIndex: Int) {
// self.theme = theme
// self._items = items
// self._selectedIndex = selectedIndex
//
// self.selectionNode = ASImageNode()
// self.selectionNode.displaysAsynchronously = false
// self.selectionNode.displayWithoutProcessing = true
//
// self.itemNodes = items.map { item in
// let itemNode = SegmentedControlItemNode()
// itemNode.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
// itemNode.titleNode.maximumNumberOfLines = 1
// itemNode.titleNode.truncationMode = .byTruncatingTail
// itemNode.setTitle(item.title, with: textFont, with: theme.textColor, for: .normal)
// itemNode.setTitle(item.title, with: selectedTextFont, with: theme.textColor, for: .selected)
// itemNode.setTitle(item.title, with: selectedTextFont, with: theme.textColor, for: [.selected, .highlighted])
// return itemNode
// }
//
// let dividersCount = items.count > 2 ? items.count - 1 : 0
// self.dividerNodes = (0 ..< dividersCount).map { _ in
// let node = ASDisplayNode()
// node.backgroundColor = theme.dividerColor
// return node
// }
//
// super.init()
//
// self.clipsToBounds = true
// self.cornerRadius = 9.0
//
// self.addSubnode(self.selectionNode)
// self.itemNodes.forEach(self.addSubnode(_:))
// self.setupButtons()
// self.dividerNodes.forEach(self.addSubnode(_:))
//
// self.backgroundColor = self.theme.backgroundColor
// self.selectionNode.image = generateSelectionImage(theme: self.theme)
// }
//
// override public func didLoad() {
// super.didLoad()
//
// self.view.disablesInteractiveTransitionGestureRecognizer = true
//
// let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
// gestureRecognizer.delegate = self
// self.view.addGestureRecognizer(gestureRecognizer)
// self.gestureRecognizer = gestureRecognizer
// }
//
// private func setupButtons() {
// for i in 0 ..< self.itemNodes.count {
// let itemNode = self.itemNodes[i]
// itemNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside)
// itemNode.highligthedChanged = { [weak self, weak itemNode] highlighted in
// if let strongSelf = self, let itemNode = itemNode {
// let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
// if strongSelf.selectedIndex == i {
// if let gestureRecognizer = strongSelf.gestureRecognizer, case .began = gestureRecognizer.state {
// } else {
// strongSelf.updateButtonsHighlights(highlightedIndex: highlighted ? i : nil, gestureSelectedIndex: strongSelf.gestureSelectedIndex)
// }
// } else if highlighted {
// transition.updateAlpha(node: itemNode, alpha: 0.4)
// }
// if !highlighted {
// transition.updateAlpha(node: itemNode, alpha: 1.0)
// }
// }
// }
// }
// }
//
// private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) {
// let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
// if highlightedIndex == nil && gestureSelectedIndex == nil {
// transition.updateTransformScale(node: self.selectionNode, scale: 1.0)
// } else {
// transition.updateTransformScale(node: self.selectionNode, scale: 0.92)
// }
// for i in 0 ..< self.itemNodes.count {
// let itemNode = self.itemNodes[i]
// if i == highlightedIndex || i == gestureSelectedIndex {
// transition.updateTransformScale(node: itemNode, scale: 0.92)
// } else {
// transition.updateTransformScale(node: itemNode, scale: 1.0)
// }
// }
// }
//
// private func updateButtonsHighlights() {
// let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
// if let gestureSelectedIndex = self.gestureSelectedIndex {
// for i in 0 ..< self.itemNodes.count {
// let itemNode = self.itemNodes[i]
// transition.updateTransformScale(node: itemNode, scale: i == gestureSelectedIndex ? 0.92 : 1.0)
// }
// } else {
// for itemNode in self.itemNodes {
// transition.updateTransformScale(node: itemNode, scale: 1.0)
// }
// }
// }
//
// public func updateTheme(_ theme: SegmentedControlTheme) {
// guard theme != self.theme else {
// return
// }
// self.theme = theme
//
// self.backgroundColor = self.theme.backgroundColor
// self.selectionNode.image = generateSelectionImage(theme: self.theme)
//
// for itemNode in self.itemNodes {
// if let title = itemNode.attributedTitle(for: .normal)?.string {
// itemNode.setTitle(title, with: textFont, with: self.theme.textColor, for: .normal)
// itemNode.setTitle(title, with: selectedTextFont, with: self.theme.textColor, for: .selected)
// itemNode.setTitle(title, with: selectedTextFont, with: self.theme.textColor, for: [.selected, .highlighted])
// }
// }
//
// for dividerNode in self.dividerNodes {
// dividerNode.backgroundColor = theme.dividerColor
// }
// }
//
// public func updateLayout(_ layout: SegmentedControlLayout, transition: ContainedViewLayoutTransition) -> CGSize {
// self.validLayout = layout
//
// let calculatedWidth: CGFloat = 0.0
//
// let width: CGFloat
// switch layout {
// case let .stretchToFill(targetWidth):
// width = targetWidth
// case let .sizeToFit(maximumWidth, minimumWidth):
// width = max(minimumWidth, min(maximumWidth, calculatedWidth))
// }
//
// let selectedIndex: Int
// if let gestureSelectedIndex = self.gestureSelectedIndex {
// selectedIndex = gestureSelectedIndex
// } else {
// selectedIndex = self.selectedIndex
// }
//
// let size = CGSize(width: width, height: 32.0)
// if !self.itemNodes.isEmpty {
// let itemSize = CGSize(width: floorToScreenPixels(size.width / CGFloat(self.itemNodes.count)), height: size.height)
//
// transition.updateBounds(node: self.selectionNode, bounds: CGRect(origin: CGPoint(), size: itemSize))
// transition.updatePosition(node: self.selectionNode, position: CGPoint(x: itemSize.width / 2.0 + itemSize.width * CGFloat(selectedIndex), y: size.height / 2.0))
//
// for i in 0 ..< self.itemNodes.count {
// let itemNode = self.itemNodes[i]
// let _ = itemNode.measure(itemSize)
// transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: itemSize.width * CGFloat(i), y: (size.height - itemSize.height) / 2.0), size: itemSize))
//
// let isSelected = selectedIndex == i
// if itemNode.isSelected != isSelected {
// if case .animated = transition {
// UIView.transition(with: itemNode.view, duration: 0.2, options: .transitionCrossDissolve, animations: {
// itemNode.isSelected = isSelected
// }, completion: nil)
// } else {
// itemNode.isSelected = isSelected
// }
// if isSelected {
// itemNode.accessibilityTraits.insert(.selected)
// } else {
// itemNode.accessibilityTraits.remove(.selected)
// }
// }
// }
// }
//
// if !self.dividerNodes.isEmpty {
// let dividerSize = CGSize(width: 1.0, height: 16.0)
// let delta: CGFloat = size.width / CGFloat(self.dividerNodes.count + 1)
// for i in 0 ..< self.dividerNodes.count {
// let dividerNode = self.dividerNodes[i]
// transition.updateFrame(node: dividerNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(delta * CGFloat(i + 1) - dividerSize.width / 2.0), y: (size.height - dividerSize.height) / 2.0), size: dividerSize))
//
// let dividerAlpha: CGFloat
// if (selectedIndex - 1 ... selectedIndex).contains(i) {
// dividerAlpha = 0.0
// } else {
// dividerAlpha = 1.0
// }
// transition.updateAlpha(node: dividerNode, alpha: dividerAlpha)
// }
// }
//
// return size
// }
//
// @objc private func buttonPressed(_ button: SegmentedControlItemNode) {
// guard let index = self.itemNodes.firstIndex(of: button) else {
// return
// }
//
// self.selectedIndexShouldChange(index, { [weak self] commit in
// if let strongSelf = self, commit {
// strongSelf._selectedIndex = index
// strongSelf.selectedIndexChanged(index)
// if let layout = strongSelf.validLayout {
// let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
// }
// }
// })
// }
//
// public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// return self.selectionNode.frame.contains(gestureRecognizer.location(in: self.view))
// }
//
// @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
// let location = recognizer.location(in: self.view)
// switch recognizer.state {
// case .changed:
// if !self.selectionNode.frame.contains(location) {
// let point = CGPoint(x: max(0.0, min(self.bounds.width, location.x)), y: 1.0)
// for i in 0 ..< self.itemNodes.count {
// let itemNode = self.itemNodes[i]
// if itemNode.frame.contains(point) {
// if i != self.gestureSelectedIndex {
// self.gestureSelectedIndex = i
// self.updateButtonsHighlights(highlightedIndex: nil, gestureSelectedIndex: i)
// if let layout = self.validLayout {
// let _ = self.updateLayout(layout, transition: .animated(duration: 0.35, curve: .slide))
// }
// }
// break
// }
// }
// }
// case .ended:
// if let gestureSelectedIndex = self.gestureSelectedIndex {
// if gestureSelectedIndex != self.selectedIndex {
// self.selectedIndexShouldChange(gestureSelectedIndex, { [weak self] commit in
// if let strongSelf = self {
// if commit {
// strongSelf._selectedIndex = gestureSelectedIndex
// strongSelf.selectedIndexChanged(gestureSelectedIndex)
// } else {
// if let layout = strongSelf.validLayout {
// let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
// }
// }
// }
// })
// }
// self.gestureSelectedIndex = nil
// }
// self.updateButtonsHighlights(highlightedIndex: nil, gestureSelectedIndex: nil)
// default:
// break
// }
// }
//}

View file

@ -51,7 +51,7 @@ public final class ContextMenuContainerNode: ASDisplayNode {
let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true)
if self.cachedMaskParams != maskParams {
let path = UIBezierPath()
let cornerRadius: CGFloat = 6.0
let cornerRadius: CGFloat = 10.0
let verticalInset: CGFloat = 9.0
let arrowWidth: CGFloat = 18.0
let requestedArrowPosition = maskParams.relativeArrowPosition

View file

@ -3,10 +3,16 @@ import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public protocol TooltipControllerCustomContentNode: ASDisplayNode {
func animateIn()
func updateLayout(size: CGSize) -> CGSize
}
public enum TooltipControllerContent: Equatable {
case text(String)
case attributedText(NSAttributedString)
case iconAndText(UIImage, String)
case custom(TooltipControllerCustomContentNode)
var text: String {
switch self {
@ -14,6 +20,8 @@ public enum TooltipControllerContent: Equatable {
return text
case let .attributedText(text):
return text.string
case .custom:
return ""
}
}
@ -23,6 +31,35 @@ public enum TooltipControllerContent: Equatable {
}
return nil
}
public static func == (lhs: TooltipControllerContent, rhs: TooltipControllerContent) -> Bool {
switch lhs {
case let .text(lhsText):
if case let .text(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .attributedText(lhsText):
if case let .attributedText(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .iconAndText(_, lhsText):
if case let .iconAndText(_, rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .custom(lhsNode):
if case let .custom(rhsNode) = rhs, lhsNode === rhsNode {
return true
} else {
return false
}
}
}
}
public enum SourceAndRect {

View file

@ -12,6 +12,7 @@ final class TooltipControllerNode: ASDisplayNode {
private let containerNode: ContextMenuContainerNode
private let imageNode: ASImageNode
private let textNode: ImmediateTextNode
private var contentNode: TooltipControllerCustomContentNode?
private let dismissByTapOutside: Bool
@ -45,10 +46,15 @@ final class TooltipControllerNode: ASDisplayNode {
self.dismiss = dismiss
if case let .custom(contentNode) = content {
self.contentNode = contentNode
}
super.init()
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.textNode)
self.contentNode.flatMap { self.containerNode.addSubnode($0) }
self.addSubnode(self.containerNode)
}
@ -71,20 +77,37 @@ final class TooltipControllerNode: ASDisplayNode {
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
let maxActionsWidth = layout.size.width - 20.0
let maxWidth = layout.size.width - 20.0
var imageSize = CGSize()
var imageSizeWithInset = CGSize()
if let image = self.imageNode.image {
imageSize = image.size
imageSizeWithInset = CGSize(width: image.size.width + 12.0, height: image.size.height)
let contentSize: CGSize
if let contentNode = self.contentNode {
contentSize = contentNode.updateLayout(size: layout.size)
contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
} else {
var imageSize = CGSize()
var imageSizeWithInset = CGSize()
if let image = self.imageNode.image {
imageSize = image.size
imageSizeWithInset = CGSize(width: image.size.width + 12.0, height: image.size.height)
}
var textSize = self.textNode.updateLayout(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
textSize.width = ceil(textSize.width / 2.0) * 2.0
textSize.height = ceil(textSize.height / 2.0) * 2.0
contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0)
let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize)
if transition.isAnimated, textFrame.size != self.textNode.frame.size {
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0))
}
let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
self.imageNode.frame = imageFrame
self.textNode.frame = textFrame
}
var textSize = self.textNode.updateLayout(CGSize(width: maxActionsWidth, height: CGFloat.greatestFiniteMagnitude))
textSize.width = ceil(textSize.width / 2.0) * 2.0
textSize.height = ceil(textSize.height / 2.0) * 2.0
let contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0)
let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize())
let insets = layout.insets(options: [.statusBar, .input])
@ -105,19 +128,11 @@ final class TooltipControllerNode: ASDisplayNode {
self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom)
self.containerNode.updateLayout(transition: transition)
let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize)
if transition.isAnimated, textFrame.size != self.textNode.frame.size {
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0))
}
let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
self.imageNode.frame = imageFrame
self.textNode.frame = textFrame
}
func animateIn() {
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.contentNode?.animateIn()
}
func animateOut(completion: @escaping () -> Void) {

View file

@ -0,0 +1,56 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InviteLinksUI",
module_name = "InviteLinksUI",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/ItemListUI:ItemListUI",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/UndoUI:UndoUI",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/SearchUI:SearchUI",
"//submodules/MergeLists:MergeLists",
"//submodules/TextFormat:TextFormat",
"//submodules/LegacyUI:LegacyUI",
"//submodules/ShareController:ShareController",
"//submodules/ContactsPeerItem:ContactsPeerItem",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/TelegramPermissionsUI:TelegramPermissionsUI",
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/PhotoResources:PhotoResources",
"//submodules/MediaResources:MediaResources",
"//submodules/NotificationSoundSelectionUI:NotificationSoundSelectionUI",
"//submodules/ContextUI:ContextUI",
"//submodules/AppBundle:AppBundle",
"//submodules/Markdown:Markdown",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/QrCode:QrCode",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/DatePickerNode:DatePickerNode",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/SectionHeaderItem:SectionHeaderItem",
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,428 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import AppBundle
import ContextUI
import TelegramStringFormatting
private final class InviteLinkEditControllerArguments {
let context: AccountContext
let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void
let dismissInput: () -> Void
let revoke: () -> Void
init(context: AccountContext, updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, dismissInput: @escaping () -> Void, revoke: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.dismissInput = dismissInput
self.revoke = revoke
}
}
private enum InviteLinksEditSection: Int32 {
case time
case usage
case revoke
}
private let invalidAmountCharacters = CharacterSet(charactersIn: "01234567890.,").inverted
func isValidNumberOfUsers(_ number: String) -> Bool {
let number = normalizeArabicNumeralString(number, type: .western)
if number.rangeOfCharacter(from: invalidAmountCharacters) != nil || number == "0" {
return false
}
return true
}
private enum InviteLinksEditEntry: ItemListNodeEntry {
case timeHeader(PresentationTheme, String)
case timePicker(PresentationTheme, InviteLinkTimeLimit)
case timeExpiryDate(PresentationTheme, Int32?)
case timeCustomPicker(PresentationTheme, Int32?)
case timeInfo(PresentationTheme, String)
case usageHeader(PresentationTheme, String)
case usagePicker(PresentationTheme, InviteLinkUsageLimit)
case usageCustomPicker(PresentationTheme, Int32?, Bool)
case usageInfo(PresentationTheme, String)
case revoke(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo:
return InviteLinksEditSection.time.rawValue
case .usageHeader, .usagePicker, .usageCustomPicker, .usageInfo:
return InviteLinksEditSection.usage.rawValue
case .revoke:
return InviteLinksEditSection.revoke.rawValue
}
}
var stableId: Int32 {
switch self {
case .timeHeader:
return 0
case .timePicker:
return 1
case .timeExpiryDate:
return 2
case .timeCustomPicker:
return 3
case .timeInfo:
return 4
case .usageHeader:
return 5
case .usagePicker:
return 6
case .usageCustomPicker:
return 7
case .usageInfo:
return 8
case .revoke:
return 9
}
}
static func ==(lhs: InviteLinksEditEntry, rhs: InviteLinksEditEntry) -> Bool {
switch lhs {
case let .timeHeader(lhsTheme, lhsText):
if case let .timeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .timePicker(lhsTheme, lhsValue):
if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeExpiryDate(lhsTheme, lhsDate):
if case let .timeExpiryDate(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate {
return true
} else {
return false
}
case let .timeCustomPicker(lhsTheme, lhsDate):
if case let .timeCustomPicker(rhsTheme, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate {
return true
} else {
return false
}
case let .timeInfo(lhsTheme, lhsText):
if case let .timeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usageHeader(lhsTheme, lhsText):
if case let .usageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .usagePicker(lhsTheme, lhsValue):
if case let .usagePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused):
if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused {
return true
} else {
return false
}
case let .usageInfo(lhsTheme, lhsText):
if case let .usageInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .revoke(lhsTheme, lhsText):
if case let .revoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinksEditEntry, rhs: InviteLinksEditEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! InviteLinkEditControllerArguments
switch self {
case let .timeHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timePicker(_, value):
return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in
arguments.updateState({ state in
var updatedState = state
if value != updatedState.time {
updatedState.pickingTimeLimit = false
}
updatedState.time = value
return updatedState
})
})
case let .timeExpiryDate(_, value):
let text: String
if let value = value {
text = stringForFullDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."))
} else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
}
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.dismissInput()
arguments.updateState { state in
var updatedState = state
updatedState.pickingTimeLimit = !state.pickingTimeLimit
return updatedState
}
})
case let .timeCustomPicker(_, date):
return ItemListDatePickerItem(presentationData: presentationData, date: date, sectionId: self.section, style: .blocks, updated: { date in
arguments.updateState({ state in
var updatedState = state
updatedState.time = .custom(date)
return updatedState
})
})
case let .timeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .usageHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usagePicker(_, value):
return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in
arguments.dismissInput()
arguments.updateState({ state in
var updatedState = state
if value != updatedState.usage {
updatedState.pickingTimeLimit = false
}
updatedState.usage = value
return updatedState
})
})
case let .usageCustomPicker(theme, value, focused):
let text = value.flatMap { String($0) } ?? (focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited)
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, tag: nil, sectionId: self.section, textUpdated: { updatedText in
guard !updatedText.isEmpty else {
return
}
arguments.updateState { state in
var updatedState = state
updatedState.usage = InviteLinkUsageLimit(value: Int32(updatedText))
return updatedState
}
}, shouldUpdateText: { text in
return isValidNumberOfUsers(text)
}, updatedFocus: { focus in
if focus {
arguments.updateState { state in
var updatedState = state
updatedState.pickingTimeLimit = false
updatedState.pickingUsageLimit = true
return updatedState
}
} else {
arguments.updateState { state in
var updatedState = state
updatedState.pickingUsageLimit = false
return updatedState
}
}
}, action: {
})
case let .usageInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .revoke(_, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.revoke()
}, tag: nil)
}
}
}
private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, presentationData: PresentationData) -> [InviteLinksEditEntry] {
var entries: [InviteLinksEditEntry] = []
entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased()))
entries.append(.timePicker(presentationData.theme, state.time))
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var time: Int32?
if case let .custom(value) = state.time {
time = value
} else if let value = state.time.value {
time = currentTime + value
}
entries.append(.timeExpiryDate(presentationData.theme, time))
if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, time ?? currentTime))
}
entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo))
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
entries.append(.usagePicker(presentationData.theme, state.usage))
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit))
entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
if let _ = invite {
entries.append(.revoke(presentationData.theme, presentationData.strings.InviteLink_Create_Revoke))
}
return entries
}
private struct InviteLinkEditControllerState: Equatable {
var usage: InviteLinkUsageLimit
var time: InviteLinkTimeLimit
var pickingTimeLimit = false
var pickingUsageLimit = false
}
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: (() -> Void)? = nil) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
let initialState: InviteLinkEditControllerState
if let invite = invite {
var usageLimit = invite.usageLimit
if let limit = usageLimit, let count = invite.count, count > 0 {
usageLimit = limit - count
}
let timeLimit: InviteLinkTimeLimit
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let expireDate = invite.expireDate {
if currentTime >= expireDate {
timeLimit = .day
} else {
timeLimit = .custom(expireDate)
}
} else {
timeLimit = .unlimited
}
initialState = InviteLinkEditControllerState(usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, pickingTimeLimit: false, pickingUsageLimit: false)
} else {
initialState = InviteLinkEditControllerState(usage: .medium, time: .week, pickingTimeLimit: false, pickingUsageLimit: false)
}
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
let arguments = InviteLinkEditControllerArguments(context: context, updateState: { f in
updateState(f)
}, dismissInput: {
dismissInputImpl?()
}, revoke: {
guard let invite = invite else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
dismissImpl?()
let _ = (revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
completion?()
})
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, nil)
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get())
|> deliverOnMainQueue
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
let expireDate: Int32?
if case let .custom(value) = state.time {
expireDate = value
} else if let value = state.time.value {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
expireDate = currentTime + value
} else {
expireDate = nil
}
let usageLimit = state.usage.value
if invite == nil {
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in
completion?()
dismissImpl?()
})
} else if let invite = invite {
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|> deliverOnMainQueue).start(next: { result in
completion?()
dismissImpl?()
})
}
})
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
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)
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
self.theme = theme
self.text = text
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 = InviteLinkHeaderItemNode()
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? InviteLinkHeaderItemNode 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 titleFont = Font.regular(13.0)
class InviteLinkHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private var item: InviteLinkHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
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)
}
}

View file

@ -0,0 +1,667 @@
import Foundation
import UIKit
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
import AsyncDisplayKit
import Postbox
import SyncCore
import TelegramCore
import Display
import AccountContext
import SolidRoundedButtonNode
import ItemListUI
import ItemListPeerItem
import SectionHeaderItem
import TelegramStringFormatting
import MergeLists
import ContextUI
import ShareController
import OverlayStatusController
import PresentationDataUtils
import DirectionalPanGesture
class InviteLinkInviteInteraction {
let context: AccountContext
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void
let manageLinks: () -> Void
init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) {
self.context = context
self.mainLinkContextAction = mainLinkContextAction
self.copyLink = copyLink
self.shareLink = shareLink
self.manageLinks = manageLinks
}
}
private struct InviteLinkInviteTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isLoading: Bool
}
private enum InviteLinkInviteEntryId: Hashable {
case mainLink
case links(Int32)
}
private enum InviteLinkInviteEntry: Comparable, Identifiable {
case mainLink(PresentationTheme, ExportedInvitation)
case links(Int32, PresentationTheme, [ExportedInvitation])
var stableId: InviteLinkInviteEntryId {
switch self {
case .mainLink:
return .mainLink
case let .links(index, _, _):
return .links(index)
}
}
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case let .mainLink(lhsTheme, lhsInvitation):
if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
return true
} else {
return false
}
case let .links(lhsIndex, lhsTheme, lhsInvitations):
if case let .links(rhsIndex, rhsTheme, rhsInvitations) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsInvitations == rhsInvitations {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case .mainLink:
switch rhs {
case .mainLink:
return false
case .links:
return true
}
case let .links(lhsIndex, _, _):
switch rhs {
case .mainLink:
return false
case let .links(rhsIndex, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self {
case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: nil, sectionId: 0, style: .plain, shareAction: {
interaction.shareLink(invite)
}, contextAction: { node in
interaction.mainLinkContextAction(invite, node, nil)
}, viewAction: {
})
case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, sectionId: 0, style: .plain, tapAction: { invite in
interaction.copyLink(invite)
}, contextAction: { invite, _ in
interaction.shareLink(invite)
})
}
}
}
private func preparedTransition(from fromEntries: [InviteLinkInviteEntry], to toEntries: [InviteLinkInviteEntry], isLoading: Bool, account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> InviteLinkInviteTransaction {
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(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return InviteLinkInviteTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
}
public final class InviteLinkInviteController: ViewController {
private var controllerNode: Node {
return self.displayNode as! Node
}
private var animatedIn = false
private let context: AccountContext
private let peerId: PeerId
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId) {
fatalError()
self.context = context
self.peerId = peerId
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, peerId: self.peerId, controller: self)
}
override public func loadView() {
super.loadView()
}
private var didAppearOnce: Bool = false
private var isDismissed: Bool = false
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didAppearOnce {
self.didAppearOnce = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.didAppearOnce = false
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: InviteLinkInviteController?
private let context: AccountContext
private let peerId: PeerId
private var interaction: InviteLinkInviteInteraction?
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var disposable: Disposable?
private let dimNode: ASDisplayNode
private let contentNode: ASDisplayNode
private let headerNode: ASDisplayNode
private let headerBackgroundNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let doneButton: HighlightableButtonNode
private let historyBackgroundNode: ASDisplayNode
private let historyBackgroundContentNode: ASDisplayNode
private var floatingHeaderOffset: CGFloat?
private let listNode: ListView
private var enqueuedTransitions: [InviteLinkInviteTransaction] = []
private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
private var revokeDisposable = MetaDisposable()
init(context: AccountContext, peerId: PeerId, controller: InviteLinkInviteController) {
self.context = context
self.peerId = peerId
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(self.presentationData)
self.controller = controller
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentNode = ASDisplayNode()
self.headerNode = ASDisplayNode()
self.headerNode.clipsToBounds = true
self.headerBackgroundNode = ASDisplayNode()
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.cornerRadius = 16.0
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.doneButton = HighlightableButtonNode()
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.historyBackgroundNode = ASDisplayNode()
self.historyBackgroundNode.isLayerBacked = true
self.historyBackgroundContentNode = ASDisplayNode()
self.historyBackgroundContentNode.isLayerBacked = true
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let invite = invite {
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let invite = invite {
let controller = InviteLinkQRCodeController(context: context, invite: invite)
self?.controller?.present(controller, in: .window(.root))
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
self?.revokeDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self?.controller?.present(controller, in: .window(.root))
})))
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
}, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
}, manageLinks: { [weak self] in
let controller = inviteLinkListController(context: context, peerId: peerId)
self?.controller?.push(controller)
self?.controller?.dismiss()
})
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId)
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view in
if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = []
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite))
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
entries.append(.mainLink(presentationData.theme, invite))
}
let previousEntries = previousEntries.swap(entries)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!)
strongSelf.enqueueTransition(transition)
}
})
self.listNode.preloadPages = true
self.listNode.stackFromBottom = true
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
}
}
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
if case let .known(value) = offset, value < 40.0 {
}
}
self.addSubnode(self.dimNode)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.historyBackgroundNode)
self.contentNode.addSubnode(self.listNode)
self.contentNode.addSubnode(self.headerNode)
self.headerNode.addSubnode(self.headerBackgroundNode)
// self.headerNode.addSubnode(self.titleNode)
self.headerNode.addSubnode(self.doneButton)
self.doneButton.addTarget(self, action: #selector(self.doneButtonPressed), forControlEvents: .touchUpInside)
self.presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings {
strongSelf.updatePresentationData(presentationData)
}
}
})
}
deinit {
self.disposable?.dispose()
self.presentationDataDisposable?.dispose()
self.revokeDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.view.disablesInteractiveModalDismiss = true
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
}
@objc private func doneButtonPressed() {
self.controller?.dismiss()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData))
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
}
private func enqueueTransition(_ transition: InviteLinkInviteTransaction) {
self.enqueuedTransitions.append(transition)
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: ListViewDeleteAndInsertOptions(), updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
func animateIn() {
guard let layout = self.validLayout else {
return
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let initialBounds = self.contentNode.bounds
self.contentNode.bounds = initialBounds.offsetBy(dx: 0.0, dy: -layout.size.height)
transition.animateView({
self.contentNode.view.bounds = initialBounds
})
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: (() -> Void)?) {
guard let layout = self.validLayout else {
return
}
var offsetCompleted = false
let internalCompletion: () -> Void = {
if offsetCompleted {
completion?()
}
}
self.contentNode.layer.animateBoundsOriginYAdditive(from: self.contentNode.bounds.origin.y, to: -layout.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var insets = UIEdgeInsets()
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0
let visibleItemsHeight: CGFloat = 147.0 + floor(52.0 * 3.5)
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = layoutTopInset + headerHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize))
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 68.0))
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let doneSize = self.doneButton.measure(CGSize(width: layout.size.width, height: headerHeight))
let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - doneSize.width - 16.0, y: 18.0), size: doneSize)
transition.updateFrame(node: self.doneButton, frame: doneFrame)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.headerNode.view {
return self.view
}
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) {
return nil
}
if point.y < self.headerNode.frame.minY {
return self.dimNode.view
}
return result
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.controller?.dismiss()
}
}
private var panGestureArguments: CGFloat?
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let contentOffset = self.listNode.visibleContentOffset()
switch recognizer.state {
case .began:
self.panGestureArguments = 0.0
case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 {
} else {
translation = currentPanOffset
}
self.panGestureArguments = translation
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.contentNode.bounds = bounds
case .ended:
let translation = recognizer.translation(in: self.contentNode.view)
var velocity = recognizer.velocity(in: self.contentNode.view)
if case let .known(value) = contentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = contentOffset {
velocity = CGPoint()
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
self.panGestureArguments = nil
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.controller?.dismiss()
} else {
var bounds = self.contentNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .cancelled:
self.panGestureArguments = nil
let previousBounds = self.contentNode.bounds
var bounds = self.contentNode.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}
}
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
self.floatingHeaderOffset = offset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let controlsHeight: CGFloat = 44.0
let listTopInset = layoutTopInset + controlsHeight
let rawControlsOffset = offset + listTopInset - controlsHeight
let controlsOffset = max(layoutTopInset, rawControlsOffset)
let isOverscrolling = rawControlsOffset <= layoutTopInset
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
let previousFrame = self.headerNode.frame
if !controlsFrame.equalTo(previousFrame) {
self.headerNode.frame = controlsFrame
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y)
}
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
if !backgroundFrame.equalTo(previousBackgroundFrame) {
self.historyBackgroundNode.frame = backgroundFrame
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
}
}
}
}

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkInviteHeaderItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0
let theme: PresentationTheme
let text: String
init(theme: PresentationTheme, text: String) {
self.theme = theme
self.text = text
}
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 = InviteLinkInviteHeaderItemNode()
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? InviteLinkInviteHeaderItemNode 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 titleFont = Font.regular(13.0)
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private var item: InviteLinkInviteHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
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)
}
}

View file

@ -0,0 +1,619 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AlertUI
import PresentationDataUtils
import AppBundle
import ContextUI
import TelegramStringFormatting
import ItemListPeerActionItem
import ShareController
private final class InviteLinkListControllerArguments {
let context: AccountContext
let shareMainLink: (ExportedInvitation?) -> Void
let openMainLink: (ExportedInvitation?) -> Void
let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let createLink: () -> Void
let openLink: (ExportedInvitation) -> Void
let linkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void
let deleteAllRevokedLinks: () -> Void
init(context: AccountContext, shareMainLink: @escaping (ExportedInvitation?) -> Void, openMainLink: @escaping (ExportedInvitation?) -> Void, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, createLink: @escaping () -> Void, openLink: @escaping (ExportedInvitation?) -> Void, linkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, deleteAllRevokedLinks: @escaping () -> Void) {
self.context = context
self.shareMainLink = shareMainLink
self.openMainLink = openMainLink
self.mainLinkContextAction = mainLinkContextAction
self.createLink = createLink
self.openLink = openLink
self.linkContextAction = linkContextAction
self.deleteAllRevokedLinks = deleteAllRevokedLinks
}
}
private enum InviteLinksListSection: Int32 {
case header
case mainLink
case links
case revokedLinks
}
private enum InviteLinksListEntry: ItemListNodeEntry {
case header(PresentationTheme, String)
case mainLinkHeader(PresentationTheme, String)
case mainLink(PresentationTheme, ExportedInvitation?, [Peer])
case linksHeader(PresentationTheme, String)
case linksCreate(PresentationTheme, String)
case links(Int32, PresentationTheme, [ExportedInvitation]?)
case linksInfo(PresentationTheme, String)
case revokedLinksHeader(PresentationTheme, String)
case revokedLinksDeleteAll(PresentationTheme, String)
case revokedLinks(Int32, PresentationTheme, [ExportedInvitation]?)
var section: ItemListSectionId {
switch self {
case .header:
return InviteLinksListSection.header.rawValue
case .mainLinkHeader, .mainLink:
return InviteLinksListSection.mainLink.rawValue
case .linksHeader, .linksCreate, .links, .linksInfo:
return InviteLinksListSection.links.rawValue
case .revokedLinksHeader, .revokedLinksDeleteAll, .revokedLinks:
return InviteLinksListSection.revokedLinks.rawValue
}
}
var stableId: Int32 {
switch self {
case .header:
return 0
case .mainLinkHeader:
return 1
case .mainLink:
return 2
case .linksHeader:
return 3
case .linksCreate:
return 4
case let .links(index, _, _):
return 5 + index
case .linksInfo:
return 10000
case .revokedLinksHeader:
return 10001
case .revokedLinksDeleteAll:
return 10002
case let .revokedLinks(index, _, _):
return 10003 + index
}
}
static func ==(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> 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 .mainLinkHeader(lhsTheme, lhsText):
if case let .mainLinkHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .mainLink(lhsTheme, lhsInvite, lhsPeers):
if case let .mainLink(rhsTheme, rhsInvite, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, arePeerArraysEqual(lhsPeers, rhsPeers) {
return true
} else {
return false
}
case let .linksHeader(lhsTheme, lhsText):
if case let .linksHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .linksCreate(lhsTheme, lhsText):
if case let .linksCreate(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .links(lhsIndex, lhsTheme, lhsLinks):
if case let .links(rhsIndex, rhsTheme, rhsLinks) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLinks == rhsLinks {
return true
} else {
return false
}
case let .linksInfo(lhsTheme, lhsText):
if case let .linksInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .revokedLinksHeader(lhsTheme, lhsText):
if case let .revokedLinksHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .revokedLinksDeleteAll(lhsTheme, lhsText):
if case let .revokedLinksDeleteAll(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .revokedLinks(lhsIndex, lhsTheme, lhsLinks):
if case let .revokedLinks(rhsIndex, rhsTheme, rhsLinks) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsLinks == rhsLinks {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinksListEntry, rhs: InviteLinksListEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! InviteLinkListControllerArguments
switch self {
case let .header(theme, text):
return InviteLinkHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareMainLink(invite)
}, contextAction: { node in
arguments.mainLinkContextAction(invite, node, nil)
}, viewAction: {
if let invite = invite {
arguments.openLink(invite)
}
})
case let .linksHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .linksCreate(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, hasSeparator: false, sectionId: self.section, editing: false, action: {
arguments.createLink()
})
case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
})
case let .linksInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .revokedLinksHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .revokedLinksDeleteAll(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, hasSeparator: false, sectionId: self.section, color: .destructive, editing: false, action: {
arguments.deleteAllRevokedLinks()
})
case let .revokedLinks(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: presentationData, invites: invites, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
}, contextAction: { invite, node in
arguments.linkContextAction(invite, node, nil)
})
}
}
}
private func inviteLinkListControllerEntries(presentationData: PresentationData, view: PeerView, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, mainPeers: [Peer]) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
entries.append(.mainLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
let mainInvite: ExportedInvitation?
if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) {
mainInvite = invite
} else if let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation {
mainInvite = invite
} else if let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation {
mainInvite = invite
} else {
mainInvite = nil
}
entries.append(.mainLink(presentationData.theme, mainInvite, mainPeers))
entries.append(.linksHeader(presentationData.theme, presentationData.strings.InviteLink_AdditionalLinks.uppercased()))
entries.append(.linksCreate(presentationData.theme, presentationData.strings.InviteLink_Create))
var additionalInvites: [ExportedInvitation]?
if let invites = invites {
additionalInvites = invites.filter { $0.link != mainInvite?.link }
}
if let additionalInvites = additionalInvites {
var index: Int32 = 0
for i in stride(from: 0, to: additionalInvites.endIndex, by: 2) {
var invitesPair: [ExportedInvitation] = []
invitesPair.append(additionalInvites[i])
if i + 1 < additionalInvites.count {
invitesPair.append(additionalInvites[i + 1])
}
entries.append(.links(index, presentationData.theme, invitesPair))
index += 1
}
}
entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
if let revokedInvites = revokedInvites, !revokedInvites.isEmpty {
entries.append(.revokedLinksHeader(presentationData.theme, presentationData.strings.InviteLink_RevokedLinks.uppercased()))
entries.append(.revokedLinksDeleteAll(presentationData.theme, presentationData.strings.InviteLink_DeleteAllRevokedLinks))
var index: Int32 = 0
for i in stride(from: 0, to: revokedInvites.endIndex, by: 2) {
var invitesPair: [ExportedInvitation] = []
invitesPair.append(revokedInvites[i])
if i + 1 < revokedInvites.count {
invitesPair.append(revokedInvites[i + 1])
}
entries.append(.revokedLinks(index, presentationData.theme, invitesPair))
index += 1
}
}
return entries
}
private struct InviteLinkListControllerState: Equatable {
var revokingPrivateLink: Bool
}
public func inviteLinkListController(context: AccountContext, peerId: PeerId) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(InviteLinkListControllerState(revokingPrivateLink: false), ignoreRepeated: true)
let stateValue = Atomic(value: InviteLinkListControllerState(revokingPrivateLink: false))
let updateState: ((InviteLinkListControllerState) -> InviteLinkListControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let revokeLinkDisposable = MetaDisposable()
actionsDisposable.add(revokeLinkDisposable)
let deleteAllRevokedLinksDisposable = MetaDisposable()
actionsDisposable.add(deleteAllRevokedLinksDisposable)
actionsDisposable.add((context.account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { view -> Signal<String?, NoError> in
return ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId)
|> mapToSignal { _ -> Signal<String?, NoError> in
return .complete()
}
}).start())
var getControllerImpl: (() -> ViewController?)?
let invitesPromise = Promise<ExportedInvitations?>()
invitesPromise.set(.single(nil)
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false)))
let revokedInvitesPromise = Promise<ExportedInvitations?>()
revokedInvitesPromise.set(.single(nil)
|> then(peerExportedInvitations(account: context.account, peerId: peerId, revoked: true)))
let arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in
if let invite = invite {
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
}
}, openMainLink: { invite in
if let invite = invite {
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
pushControllerImpl?(controller)
}
}, mainLinkContextAction: { invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let invite = invite {
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
if let invite = invite {
let controller = InviteLinkQRCodeController(context: context, invite: invite)
presentControllerImpl?(controller, nil)
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in
if !state.revokingPrivateLink {
revoke = true
var updatedState = state
updatedState.revokingPrivateLink = true
return updatedState
} else {
return state
}
}
if revoke {
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})))
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}, createLink: {
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: nil, completion: {
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
})
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
}, openLink: { invite in
if let invite = invite {
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, importersContext: nil)
pushControllerImpl?(controller)
}
}, linkContextAction: { invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
})))
if !invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = InviteLinkQRCodeController(context: context, invite: invite)
presentControllerImpl?(controller, nil)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = inviteLinkEditController(context: context, peerId: peerId, invite: invite, completion: {
invitesPromise.set(peerExportedInvitations(account: context.account, peerId: peerId, revoked: false))
})
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
})))
}
if invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextDelete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})))
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
revokeLinkDisposable.set((revokePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}, deleteAllRevokedLinks: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.InviteLink_DeleteAllRevokedLinksAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteAllRevokedLinksAlert_Action, color: .destructive, action: {
dismissAction()
deleteAllRevokedLinksDisposable.set((deleteAllRevokedPeerExportedInvitations(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
}))
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
let peerView = context.account.viewTracker.peerView(peerId)
|> deliverOnMainQueue
let importersState = Promise<PeerInvitationImportersState?>(nil)
let importersContext: Signal<PeerInvitationImportersContext, NoError> = peerView
|> mapToSignal { view -> Signal<ExportedInvitation, NoError> in
if let cachedData = view.cachedData as? CachedGroupData, let exportedInvitation = cachedData.exportedInvitation {
return .single(exportedInvitation)
} else if let cachedData = view.cachedData as? CachedChannelData, let exportedInvitation = cachedData.exportedInvitation {
return .single(exportedInvitation)
} else {
return .complete()
}
}
|> distinctUntilChanged
|> deliverOnMainQueue
|> map { invite -> PeerInvitationImportersContext in
return PeerInvitationImportersContext(account: context.account, peerId: peerId, invite: invite)
} |> afterNext { context in
importersState.set(context.state |> map(Optional.init))
}
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesPromise.get(), revokedInvitesPromise.get())
|> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.InviteLink_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites?.list, revokedInvites: revokedInvites?.list, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
controller.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
}
}
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
presentInGlobalOverlayImpl = { [weak controller] c in
if let controller = controller {
controller.presentInGlobalOverlay(c)
}
}
getControllerImpl = { [weak controller] in
return controller
}
return controller
}
final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = true
self.blurBackground = false
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View file

@ -0,0 +1,414 @@
import Foundation
import UIKit
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
import AsyncDisplayKit
import SyncCore
import Display
import QrCode
import AccountContext
import SolidRoundedButtonNode
import AnimatedStickerNode
private func shareQrCode(context: AccountContext, link: String) {
let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Chat/Links/QrLogo")))
|> map { _, generator -> UIImage? in
let imageSize = CGSize(width: 768.0, height: 768.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage()
}
|> deliverOnMainQueue).start(next: { image in
guard let image = image else {
return
}
let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
context.sharedContext.applicationBindings.presentNativeController(activityController)
})
}
public final class InviteLinkQRCodeController: ViewController {
private var controllerNode: Node {
return self.displayNode as! Node
}
private var animatedIn = false
private let context: AccountContext
private let invite: ExportedInvitation
private var presentationDataDisposable: Disposable?
private let idleTimerExtensionDisposable = MetaDisposable()
public init(context: AccountContext, invite: ExportedInvitation) {
self.context = context
self.invite = invite
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.idleTimerExtensionDisposable.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, invite: self.invite)
self.controllerNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.cancel = { [weak self] in
self?.dismiss()
}
}
override public func loadView() {
super.loadView()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.animateOut(completion: completion)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
class Node: ViewControllerTracingNode, UIScrollViewDelegate {
private let context: AccountContext
private let invite: ExportedInvitation
private var presentationData: PresentationData
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let contentContainerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let titleNode: ASTextNode
private let subtitleNode: ASTextNode
private let cancelButton: HighlightableButtonNode
private let textNode: ImmediateTextNode
private let qrButtonNode: HighlightTrackingButtonNode
private let qrImageNode: TransformImageNode
private let qrIconNode: AnimatedStickerNode
private var qrCodeSize: Int?
private let buttonNode: SolidRoundedButtonNode
private var containerLayout: (ContainerViewLayout, CGFloat)?
var completion: ((Int32) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
init(context: AccountContext, invite: ExportedInvitation) {
self.context = context
self.invite = invite
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 16.0
let backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
let textColor = self.presentationData.theme.actionSheet.primaryTextColor
let secondaryTextColor = self.presentationData.theme.actionSheet.secondaryTextColor
let accentColor = self.presentationData.theme.actionSheet.controlAccentColor
self.contentBackgroundNode = ASDisplayNode()
self.contentBackgroundNode.backgroundColor = backgroundColor
self.titleNode = ASTextNode()
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Title, font: Font.bold(17.0), textColor: textColor)
self.subtitleNode = ASTextNode()
self.subtitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Title, font: Font.regular(13.0), textColor: secondaryTextColor)
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: accentColor, for: .normal)
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 3
self.textNode.textAlignment = .center
self.qrButtonNode = HighlightTrackingButtonNode()
self.qrImageNode = TransformImageNode()
self.qrIconNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "PlaneLogo", ofType: "tgs") {
self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 240, height: 240, mode: .direct(cachePathPrefix: nil))
self.qrIconNode.visibility = true
}
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.backgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.backgroundNode.addSubnode(self.contentBackgroundNode)
self.contentContainerNode.addSubnode(self.titleNode)
self.contentContainerNode.addSubnode(self.cancelButton)
self.contentContainerNode.addSubnode(self.buttonNode)
self.contentContainerNode.addSubnode(self.textNode)
self.contentContainerNode.addSubnode(self.qrImageNode)
self.contentContainerNode.addSubnode(self.qrIconNode)
self.contentContainerNode.addSubnode(self.qrButtonNode)
let textFont = Font.regular(16.0)
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_QRCode_Info, font: textFont, textColor: secondaryTextColor)
self.buttonNode.title = self.presentationData.strings.InviteLink_QRCode_Share
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.buttonNode.pressed = { [weak self] in
if let strongSelf = self{
shareQrCode(context: strongSelf.context, link: strongSelf.invite.link)
}
}
self.qrImageNode.setSignal(qrCode(string: self.invite.link, color: .black, backgroundColor: .white, icon: .cutout) |> beforeNext { [weak self] size, _ in
guard let strongSelf = self else {
return
}
strongSelf.qrCodeSize = size
if let (layout, navigationHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
} |> map { $0.1 }, attemptSynchronously: true)
self.qrButtonNode.addTarget(self, action: #selector(self.qrPressed), forControlEvents: .touchUpInside)
self.qrButtonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.qrImageNode.alpha = 0.4
strongSelf.qrIconNode.alpha = 0.4
} else {
strongSelf.qrImageNode.layer.animateAlpha(from: strongSelf.qrImageNode.alpha, to: 1.0, duration: 0.2)
strongSelf.qrImageNode.alpha = 1.0
strongSelf.qrIconNode.layer.animateAlpha(from: strongSelf.qrIconNode.alpha, to: 1.0, duration: 0.2)
strongSelf.qrIconNode.alpha = 1.0
}
}
}
@objc private func qrPressed() {
shareQrCode(context: self.context, link: self.invite.link)
}
func updatePresentationData(_ presentationData: PresentationData) {
let previousTheme = self.presentationData.theme
self.presentationData = presentationData
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
self.cancelButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancelButtonPressed()
}
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
return self.dimNode.view
}
}
return super.hitTest(point, with: event)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar, .input])
let cleanInsets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let makeImageLayout = self.qrImageNode.asyncLayout()
let imageSide: CGFloat = 240.0
let imageSize = CGSize(width: imageSide, height: imageSide)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
let _ = imageApply()
let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: insets.top + 24.0), size: imageSize)
transition.updateFrame(node: self.qrImageNode, frame: imageFrame)
transition.updateFrame(node: self.qrButtonNode, frame: imageFrame)
if let qrCodeSize = self.qrCodeSize {
let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil)
self.qrIconNode.updateLayout(size: cutoutFrame.size)
transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size))
transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0))
}
let inset: CGFloat = 22.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude))
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: imageFrame.maxX + 20.0), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let buttonSideInset: CGFloat = 16.0
let bottomInset = insets.bottom + 10.0
let buttonWidth = layout.size.width - buttonSideInset * 2.0
let buttonHeight: CGFloat = 50.0
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
let titleHeight: CGFloat = 54.0
let contentHeight = titleHeight + textSize.height + imageSize.height + bottomInset + 52.0 + 77.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
let sideInset = floor((layout.size.width - width) / 2.0)
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
let contentFrame = contentContainerFrame
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 16.0, y: 16.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
let doneButtonHeight = self.buttonNode.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: doneButtonHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
}
}
}

View file

@ -0,0 +1,766 @@
import Foundation
import UIKit
import SwiftSignalKit
import TelegramPresentationData
import AppBundle
import AsyncDisplayKit
import Postbox
import SyncCore
import TelegramCore
import Display
import AccountContext
import SolidRoundedButtonNode
import ItemListUI
import ItemListPeerItem
import SectionHeaderItem
import TelegramStringFormatting
import MergeLists
import ContextUI
import ShareController
import OverlayStatusController
import PresentationDataUtils
import DirectionalPanGesture
class InviteLinkViewInteraction {
let context: AccountContext
let openPeer: (PeerId) -> Void
let shareLink: (ExportedInvitation) -> Void
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.openPeer = openPeer
self.shareLink = shareLink
self.contextAction = contextAction
}
}
private struct InviteLinkViewTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isLoading: Bool
}
private enum InviteLinkViewEntryId: Hashable {
case link
case creatorHeader
case creator
case importerHeader
case importer(PeerId)
}
private func color(for invite: ExportedInvitation) -> UIColor? {
let color: UIColor?
let availability = invitationAvailability(invite)
if invite.isRevoked {
color = nil
} else if invite.expireDate == nil && invite.usageLimit == nil {
color = nil
} else if availability >= 0.5 {
color = UIColor(rgb: 0x4aca62)
} else if availability > 0.0 {
color = UIColor(rgb: 0xf8a953)
} else {
color = UIColor(rgb: 0xf2656a)
}
return color
}
private enum InviteLinkViewEntry: Comparable, Identifiable {
case link(PresentationTheme, ExportedInvitation)
case creatorHeader(PresentationTheme, String)
case creator(PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
case importerHeader(PresentationTheme, String)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, Peer, Int32)
var stableId: InviteLinkViewEntryId {
switch self {
case .link:
return .link
case .creatorHeader:
return .creatorHeader
case .creator:
return .creator
case .importerHeader:
return .importerHeader
case let .importer(_, _, _, peer, _):
return .importer(peer.id)
}
}
static func ==(lhs: InviteLinkViewEntry, rhs: InviteLinkViewEntry) -> Bool {
switch lhs {
case let .link(lhsTheme, lhsInvitation):
if case let .link(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation {
return true
} else {
return false
}
case let .creatorHeader(lhsTheme, lhsTitle):
if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .creator(lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate):
if case let .creator(rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
return true
} else {
return false
}
case let .importerHeader(lhsTheme, lhsTitle):
if case let .importerHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinkViewEntry, rhs: InviteLinkViewEntry) -> Bool {
switch lhs {
case .link:
switch rhs {
case .link:
return false
case .creatorHeader, .creator, .importerHeader, .importer:
return true
}
case .creatorHeader:
switch rhs {
case .link, .creatorHeader:
return false
case .creator, .importerHeader, .importer:
return true
}
case .creator:
switch rhs {
case .link, .creatorHeader, .creator:
return false
case .importerHeader, .importer:
return true
}
case .importerHeader:
switch rhs {
case .link, .creatorHeader, .importerHeader:
return false
case .creator, .importer:
return true
}
case let .importer(lhsIndex, _, _, _, _):
switch rhs {
case .link, .creatorHeader, .creator, .importerHeader:
return false
case let .importer(rhsIndex, _, _, _, _):
return lhsIndex < rhsIndex
}
}
}
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> ListViewItem {
switch self {
case let .link(_, invite):
let buttonColor = color(for: invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], buttonColor: buttonColor, sectionId: 0, style: .plain, shareAction: {
interaction.shareLink(invite)
}, contextAction: { node in
interaction.contextAction(invite, node, nil)
}, viewAction: {
})
case let .creatorHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .creator(_, dateTimeFormat, peer, date):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
case let .importerHeader(_, title):
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .importer(_, _, dateTimeFormat, peer, date):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil)
}
}
}
private func preparedTransition(from fromEntries: [InviteLinkViewEntry], to toEntries: [InviteLinkViewEntry], isLoading: Bool, account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> InviteLinkViewTransaction {
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(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return InviteLinkViewTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
}
public final class InviteLinkViewController: ViewController {
private var controllerNode: Node {
return self.displayNode as! Node
}
private var animatedIn = false
private let context: AccountContext
private let peerId: PeerId
private let invite: ExportedInvitation
private let importersContext: PeerInvitationImportersContext?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?) {
self.context = context
self.peerId = peerId
self.invite = invite
self.importersContext = importersContext
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.statusBar.statusBarStyle = .Ignore
self.blocksBackgroundWhenInOverlay = true
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, peerId: self.peerId, invite: self.invite, importersContext: self.importersContext, controller: self)
}
override public func loadView() {
super.loadView()
}
private var didAppearOnce: Bool = false
private var isDismissed: Bool = false
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.didAppearOnce {
self.didAppearOnce = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.didAppearOnce = false
self.controllerNode.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
}
class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: InviteLinkViewController?
private let context: AccountContext
private let peerId: PeerId
private let invite: ExportedInvitation
private var interaction: InviteLinkViewInteraction?
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var disposable: Disposable?
private let dimNode: ASDisplayNode
private let contentNode: ASDisplayNode
private let headerNode: ASDisplayNode
private let headerBackgroundNode: ASDisplayNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let editButton: HighlightableButtonNode
private let doneButton: HighlightableButtonNode
private let historyBackgroundNode: ASDisplayNode
private let historyBackgroundContentNode: ASDisplayNode
private var floatingHeaderOffset: CGFloat?
private let listNode: ListView
private var enqueuedTransitions: [InviteLinkViewTransaction] = []
private var validLayout: ContainerViewLayout?
private var presentationDataDisposable: Disposable?
private let importersContext: PeerInvitationImportersContext
init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, importersContext: PeerInvitationImportersContext?, controller: InviteLinkViewController) {
self.context = context
self.peerId = peerId
self.invite = invite
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.controller = controller
self.importersContext = importersContext ?? PeerInvitationImportersContext(account: context.account, peerId: peerId, invite: invite)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.contentNode = ASDisplayNode()
self.headerNode = ASDisplayNode()
self.headerNode.clipsToBounds = true
self.headerBackgroundNode = ASDisplayNode()
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.cornerRadius = 16.0
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.textAlignment = .center
let buttonColor = color(for: invite) ?? presentationData.theme.actionSheet.controlAccentColor
self.editButton = HighlightableButtonNode()
self.editButton.setTitle(self.presentationData.strings.Common_Edit, with: Font.regular(17.0), with: buttonColor, for: .normal)
self.doneButton = HighlightableButtonNode()
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: buttonColor, for: .normal)
self.historyBackgroundNode = ASDisplayNode()
self.historyBackgroundNode.isLayerBacked = true
self.historyBackgroundContentNode = ASDisplayNode()
self.historyBackgroundContentNode.isLayerBacked = true
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode)
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.interaction = InviteLinkViewInteraction(context: context, openPeer: { [weak self] peerId in
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId), keepStack: .always))
}
}, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root))
}, contextAction: { [weak self] invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root))
})))
if !invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
let controller = InviteLinkQRCodeController(context: context, invite: invite)
self?.controller?.present(controller, in: .window(.root))
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController)
})
let previousEntries = Atomic<[InviteLinkViewEntry]?>(value: nil)
let creatorPeer = context.account.postbox.loadedPeerWithId(invite.adminId)
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.importersContext.state, creatorPeer)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, creatorPeer in
if let strongSelf = self {
var entries: [InviteLinkViewEntry] = []
entries.append(.link(presentationData.theme, invite))
entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased()))
entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, invite.date))
if !state.importers.isEmpty {
entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
}
var index: Int32 = 0
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date))
}
index += 1
}
let previousEntries = previousEntries.swap(entries)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!)
strongSelf.enqueueTransition(transition)
}
})
self.listNode.preloadPages = true
self.listNode.stackFromBottom = true
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in
if let strongSelf = self {
strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition)
}
}
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
if case let .known(value) = offset, value < 40.0 {
self?.importersContext.loadMore()
}
}
self.addSubnode(self.dimNode)
self.addSubnode(self.contentNode)
self.contentNode.addSubnode(self.historyBackgroundNode)
self.contentNode.addSubnode(self.listNode)
self.contentNode.addSubnode(self.headerNode)
self.headerNode.addSubnode(self.headerBackgroundNode)
self.headerNode.addSubnode(self.titleNode)
self.headerNode.addSubnode(self.subtitleNode)
self.headerNode.addSubnode(self.editButton)
self.headerNode.addSubnode(self.doneButton)
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), forControlEvents: .touchUpInside)
self.doneButton.addTarget(self, action: #selector(self.doneButtonPressed), forControlEvents: .touchUpInside)
self.presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings {
strongSelf.updatePresentationData(presentationData)
}
}
})
if invite.isRevoked {
self.editButton.isHidden = true
}
}
deinit {
self.disposable?.dispose()
self.presentationDataDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.view.disablesInteractiveModalDismiss = true
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
}
@objc private func editButtonPressed() {
let navigationController = self.controller?.navigationController as? NavigationController
self.controller?.dismiss()
if let navigationController = navigationController {
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite)
controller.navigationPresentation = .modal
navigationController.pushViewController(controller)
}
}
@objc private func doneButtonPressed() {
self.controller?.dismiss()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData))
self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
let buttonColor = color(for: invite) ?? self.presentationData.theme.actionSheet.controlAccentColor
self.editButton.setTitle(self.presentationData.strings.Common_Edit, with: Font.regular(17.0), with: buttonColor, for: .normal)
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: buttonColor, for: .normal)
}
private func enqueueTransition(_ transition: InviteLinkViewTransaction) {
self.enqueuedTransitions.append(transition)
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: ListViewDeleteAndInsertOptions(), updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
func animateIn() {
guard let layout = self.validLayout else {
return
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let initialBounds = self.contentNode.bounds
self.contentNode.bounds = initialBounds.offsetBy(dx: 0.0, dy: -layout.size.height)
transition.animateView({
self.contentNode.view.bounds = initialBounds
})
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut(completion: (() -> Void)?) {
guard let layout = self.validLayout else {
return
}
var offsetCompleted = false
let internalCompletion: () -> Void = {
if offsetCompleted {
completion?()
}
}
self.contentNode.layer.animateBoundsOriginYAdditive(from: self.contentNode.bounds.origin.y, to: -layout.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var insets = UIEdgeInsets()
insets.left = layout.safeInsets.left
insets.right = layout.safeInsets.right
insets.bottom = layout.intrinsicInsets.bottom
let headerHeight: CGFloat = 54.0
let visibleItemsHeight: CGFloat = 147.0 + floor(52.0 * 3.5)
let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top)
let listTopInset = layoutTopInset + headerHeight
let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset)
insets.top = max(0.0, listNodeSize.height - visibleItemsHeight)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize))
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 68.0))
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight))
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
let editSize = self.editButton.measure(CGSize(width: layout.size.width, height: headerHeight))
let editFrame = CGRect(origin: CGPoint(x: 16.0, y: 18.0), size: editSize)
transition.updateFrame(node: self.editButton, frame: editFrame)
let doneSize = self.doneButton.measure(CGSize(width: layout.size.width, height: headerHeight))
let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - doneSize.width - 16.0, y: 18.0), size: doneSize)
transition.updateFrame(node: self.doneButton, frame: doneFrame)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.headerNode.view {
return self.view
}
if result === self.headerNode.view {
return self.view
}
if !self.bounds.contains(point) {
return nil
}
if point.y < self.headerNode.frame.minY {
return self.dimNode.view
}
return result
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.controller?.dismiss()
}
}
private var panGestureArguments: CGFloat?
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let contentOffset = self.listNode.visibleContentOffset()
switch recognizer.state {
case .began:
self.panGestureArguments = 0.0
case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 {
} else {
translation = currentPanOffset
}
self.panGestureArguments = translation
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.contentNode.bounds = bounds
case .ended:
let translation = recognizer.translation(in: self.contentNode.view)
var velocity = recognizer.velocity(in: self.contentNode.view)
if case let .known(value) = contentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = contentOffset {
velocity = CGPoint()
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
self.panGestureArguments = nil
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.controller?.dismiss()
} else {
var bounds = self.contentNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .cancelled:
self.panGestureArguments = nil
let previousBounds = self.contentNode.bounds
var bounds = self.contentNode.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}
}
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
self.floatingHeaderOffset = offset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let controlsHeight: CGFloat = 44.0
let listTopInset = layoutTopInset + controlsHeight
let rawControlsOffset = offset + listTopInset - controlsHeight
let controlsOffset = max(layoutTopInset, rawControlsOffset)
let isOverscrolling = rawControlsOffset <= layoutTopInset
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
let previousFrame = self.headerNode.frame
if !controlsFrame.equalTo(previousFrame) {
self.headerNode.frame = controlsFrame
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y)
}
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
if !backgroundFrame.equalTo(previousBackgroundFrame) {
self.historyBackgroundNode.frame = backgroundFrame
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
}
}
}
}

View file

@ -0,0 +1,504 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import SyncCore
import TelegramPresentationData
import ItemListUI
import SolidRoundedButtonNode
import RadialStatusNode
private let itemSpacing: CGFloat = 10.0
private let titleFont = Font.semibold(17.0)
private let subtitleFont = Font.regular(12.0)
private func generateBackgroundImage(colors: NSArray) -> UIImage? {
return generateImage(CGSize(width: 45, height: 45), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 15)
context.addPath(path.cgPath)
context.clip()
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
})?.stretchableImage(withLeftCapWidth: 22, topCapHeight: 22)
}
func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var availability: CGFloat = 1.0
if let expireDate = invite.expireDate {
let startDate = invite.startDate ?? invite.date
let fraction = CGFloat(expireDate - currentTime) / CGFloat(expireDate - startDate)
availability = min(fraction, availability)
}
if let usageLimit = invite.usageLimit, let count = invite.count {
let fraction = 1.0 - (CGFloat(count) / CGFloat(usageLimit))
availability = min(fraction, availability)
}
return availability
}
private class ItemNode: ASDisplayNode {
private let backgroundNode: ASImageNode
private let iconNode: ASImageNode
private var timerNode: TimerNode?
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonIconNode: ASImageNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData)?
var action: (() -> Void)?
var contextAction: ((ASDisplayNode) -> Void)?
override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.buttonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.buttonIconNode = ASImageNode()
self.buttonIconNode.displaysAsynchronously = false
self.buttonIconNode.displayWithoutProcessing = true
self.buttonIconNode.image = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 11.0), size: CGSize(width: 4.0, height: 4.0)))
})
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.buttonIconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.buttonNode.addSubnode(self.containerNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonIconNode.alpha = 0.4
} else {
strongSelf.buttonIconNode.alpha = 1.0
strongSelf.buttonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
override func didLoad() {
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
self.action?()
}
@objc private func buttonPressed() {
self.contextAction?(self.extractedContainerNode)
}
func update(size: CGSize, wide: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
let updated = self.params?.size != size || self.params?.wide != wide || self.params?.invite != invite
self.params = (size, wide, invite, presentationData)
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = invitationAvailability(invite)
var isExpired = false
let secondaryTextColor: UIColor
if invite.isRevoked {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xd4d8db).cgColor, UIColor(rgb: 0xced2d5).cgColor])
secondaryTextColor = UIColor(rgb: 0xf8f9f9)
} else if invite.expireDate == nil && invite.usageLimit == nil {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x00b5f7).cgColor, UIColor(rgb: 0x00b2f6).cgColor])
secondaryTextColor = UIColor(rgb: 0xa7f4ff)
} else if availability >= 0.5 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0x4aca62).cgColor, UIColor(rgb: 0x43c85c).cgColor])
secondaryTextColor = UIColor(rgb: 0xc5ffe6)
} else if availability > 0.0 {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf8a953).cgColor, UIColor(rgb: 0xf7a64e).cgColor])
secondaryTextColor = UIColor(rgb: 0xfeffd7)
} else {
self.backgroundNode.image = generateBackgroundImage(colors: [UIColor(rgb: 0xf2656a).cgColor, UIColor(rgb: 0xf25f65).cgColor])
secondaryTextColor = UIColor(rgb: 0xffd3de)
isExpired = true
}
let itemWidth = wide ? size.width : floor((size.width - itemSpacing) / 2.0)
var inviteLink = invite.link.replacingOccurrences(of: "https://", with: "")
if !wide {
inviteLink = inviteLink.replacingOccurrences(of: "joinchat/", with: "joinchat/\n")
inviteLink = inviteLink.replacingOccurrences(of: "join/", with: "join/\n")
}
let title: NSMutableAttributedString = NSMutableAttributedString(string: inviteLink, font: titleFont, textColor: UIColor.white)
if inviteLink.hasPrefix("t.me/joinchat/") {
title.addAttribute(NSAttributedString.Key.foregroundColor, value: secondaryTextColor, range: NSMakeRange(0, "t.me/joinchat/".count))
} else if inviteLink.hasPrefix("t.me/join/") {
title.addAttribute(NSAttributedString.Key.foregroundColor, value: secondaryTextColor, range: NSMakeRange(0, "t.me/join/".count))
}
self.titleNode.attributedText = title
var subtitleText: String = ""
if let count = invite.count {
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else {
subtitleText = isExpired || invite.isRevoked ? presentationData.strings.InviteLink_PeopleJoinedShortNoneExpired : presentationData.strings.InviteLink_PeopleJoinedShortNone
}
if invite.isRevoked {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += presentationData.strings.InviteLink_Revoked
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
} else if let expireDate = invite.expireDate, currentTime > expireDate {
if !subtitleText.isEmpty {
subtitleText += ""
}
subtitleText += presentationData.strings.InviteLink_Expired
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Expired"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
} else if let expireDate = invite.expireDate {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Flame"), color: .white)
let timerNode: TimerNode
if let current = self.timerNode {
timerNode = current
} else {
timerNode = TimerNode()
self.timerNode = timerNode
self.addSubnode(timerNode)
}
timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate)
} else {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
}
self.iconNode.frame = CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0)
self.timerNode?.frame = CGRect(x: 8.0, y: 8.0, width: 34.0, height: 34.0)
let subtitle: NSMutableAttributedString = NSMutableAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
self.subtitleNode.attributedText = subtitle
let titleSize = self.titleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: itemWidth - 24.0, height: 100.0))
self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 52.0), size: titleSize)
self.subtitleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 52.0 + titleSize.height + 3.0), size: subtitleSize)
let itemSize = CGSize(width: itemWidth, height: wide ? 102.0 : 122.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: itemSize)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let buttonSize = CGSize(width: 26.0, height: 26.0)
let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize)
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: buttonSize)
self.extractedContainerNode.contentRect = CGRect(origin: CGPoint(), size: buttonSize)
self.buttonIconNode.frame = CGRect(origin: CGPoint(), size: buttonSize)
return itemSize
}
}
class InviteLinksGridNode: ASDisplayNode {
private var items: [ExportedInvitation] = []
private var itemNodes: [String: ItemNode] = [:]
var action: ((ExportedInvitation) -> Void)?
var contextAction: ((ASDisplayNode, ExportedInvitation) -> Void)?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result
}
func update(size: CGSize, safeInset: CGFloat, items: [ExportedInvitation], presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
self.items = items
var contentSize: CGSize = size
var contentHeight: CGFloat = 0.0
let sideInset: CGFloat = 16.0 + safeInset
var validIds = Set<String>()
for i in 0 ..< self.items.count {
let invite = self.items[i]
validIds.insert(invite.link)
var itemNode: ItemNode?
var wasAdded = false
if let current = self.itemNodes[invite.link] {
itemNode = current
} else {
wasAdded = true
let addedItemNode = ItemNode()
itemNode = addedItemNode
self.itemNodes[invite.link] = addedItemNode
self.addSubnode(addedItemNode)
}
if let itemNode = itemNode {
let col = CGFloat(i % 2)
let row = floor(CGFloat(i) / 2.0)
let wide = (i == self.items.count - 1 && (self.items.count % 2) != 0)
let itemSize = itemNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: size.height), wide: wide, invite: invite, presentationData: presentationData, transition: transition)
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 4.0 + row * (122.0 + itemSpacing)), size: itemSize)
if !wide && col > 0 {
itemFrame.origin.x += itemSpacing + itemSize.width
}
contentHeight = max(contentHeight, itemFrame.maxY + itemSpacing)
if wasAdded {
itemNode.frame = itemFrame
} else {
transition.updateFrame(node: itemNode, frame: itemFrame)
}
itemNode.action = { [weak self] in
self?.action?(invite)
}
itemNode.contextAction = { [weak self] node in
self?.contextAction?(node, invite)
}
}
}
var removeIds: [String] = []
for (id, _) in self.itemNodes {
if !validIds.contains(id) {
removeIds.append(id)
}
}
for id in removeIds {
if let itemNode = self.itemNodes.removeValue(forKey: id) {
itemNode.removeFromSupernode()
}
}
contentSize.height = contentHeight
return contentSize
}
}
private struct ContentParticle {
var position: CGPoint
var direction: CGPoint
var velocity: CGFloat
var alpha: CGFloat
var lifetime: Double
var beginTime: Double
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
self.position = position
self.direction = direction
self.velocity = velocity
self.alpha = alpha
self.lifetime = lifetime
self.beginTime = beginTime
}
}
private final class TimerNode: ASDisplayNode {
private struct Params: Equatable {
var color: UIColor
var creationTimestamp: Int32
var deadlineTimestamp: Int32
}
private let hierarchyTrackingNode: HierarchyTrackingNode
private var inHierarchyValue: Bool = false
private var animator: ConstantDisplayLinkAnimator?
private let contentNode: ASDisplayNode
private var particles: [ContentParticle] = []
private var currentParams: Params?
var reachedTimeout: (() -> Void)?
override init() {
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
self.contentNode = ASDisplayNode()
super.init()
self.addSubnode(self.contentNode)
updateInHierarchy = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.inHierarchyValue = value
strongSelf.animator?.isPaused = value
}
}
deinit {
self.animator?.invalidate()
}
func update(color: UIColor, creationTimestamp: Int32, deadlineTimestamp: Int32) {
let params = Params(
color: color,
creationTimestamp: creationTimestamp,
deadlineTimestamp: deadlineTimestamp
)
self.currentParams = params
self.updateValues()
}
private func updateValues() {
guard let params = self.currentParams else {
return
}
let color = params.color
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp)
fraction = 1.0 - max(0.0, min(0.94, fraction))
let image: UIImage?
let diameter: CGFloat = 26.0
let inset: CGFloat = 8.0
let lineWidth: CGFloat = 2.0
let timestamp = CACurrentMediaTime()
let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0)
let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0
let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
context.setFillColor(color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addPath(path)
context.strokePath()
for particle in self.particles {
let size: CGFloat = 2.0
context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
})
self.contentNode.contents = image?.cgImage
if let image = image {
self.contentNode.frame = CGRect(origin: CGPoint(), size: image.size)
}
if fraction <= .ulpOfOne {
self.animator?.invalidate()
self.animator = nil
} else {
if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateValues()
})
self.animator = animator
animator.isPaused = self.inHierarchyValue
}
}
}
}

View file

@ -0,0 +1,241 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import SyncCore
import TelegramPresentationData
import ItemListUI
public class ItemListDatePickerItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let date: Int32?
public let sectionId: ItemListSectionId
let style: ItemListStyle
let updated: ((Int32) -> Void)?
public let tag: ItemListItemTag?
public init(
presentationData: ItemListPresentationData,
date: Int32?,
sectionId: ItemListSectionId,
style: ItemListStyle,
updated: ((Int32) -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.date = date
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 = ItemListDatePickerItemNode()
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? ItemListDatePickerItemNode {
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()
})
}
}
}
}
}
public var selectable: Bool = false
}
public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let datePicker: UIDatePicker
private var item: ItemListDatePickerItem?
override public var canBeSelected: Bool {
return false
}
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.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.datePicker = UIDatePicker()
self.datePicker.minimumDate = Date()
self.datePicker.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
self.datePicker.preferredDatePickerStyle = .inline
}
super.init(layerBacked: false, dynamicBounce: false)
self.datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func didLoad() {
super.didLoad()
self.view.addSubview(self.datePicker)
}
@objc private func datePickerUpdated() {
self.item?.updated?(Int32(self.datePicker.date.timeIntervalSince1970))
}
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let height: CGFloat = 360.0
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors)
}
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [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.datePicker.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
strongSelf.datePicker.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
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, 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
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 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: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
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)
}
}

View file

@ -0,0 +1,409 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
enum InviteLinkTimeLimit: Equatable {
case hour
case day
case week
case unlimited
case custom(Int32)
init(position: Int) {
switch position {
case 0:
self = .hour
case 1:
self = .day
case 2:
self = .week
default:
self = .unlimited
}
}
var value: Int32? {
switch self {
case .hour:
return 3600
case .day:
return 86400
case .week:
return 604800
case .unlimited:
return nil
case let .custom(value):
return value
}
}
var position: Int {
switch self {
case .hour:
return 0
case .day:
return 1
case .week:
return 2
case .unlimited:
return 3
case let .custom(value):
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let relativeValue = value - currentTime
if relativeValue < 3600 {
return 0
} else if relativeValue < 86400 {
return 1
} else if relativeValue < 604800 {
return 2
} else {
return 3
}
}
}
}
final class ItemListInviteLinkTimeLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: InviteLinkTimeLimit
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (InviteLinkTimeLimit) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.enabled = enabled
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 = ItemListInviteLinkTimeLimitItemNode()
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? ItemListInviteLinkTimeLimitItemNode {
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 generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
private final class ItemListInviteLinkTimeLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lowTextNode: TextNode
private let mediumTextNode: TextNode
private let highTextNode: TextNode
private let unlimitedTextNode: TextNode
private let customTextNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: ItemListInviteLinkTimeLimitItem?
private var layoutParams: ListViewItemLayoutParams?
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.lowTextNode = TextNode()
self.lowTextNode.isUserInteractionEnabled = false
self.lowTextNode.displaysAsynchronously = false
self.mediumTextNode = TextNode()
self.mediumTextNode.isUserInteractionEnabled = false
self.mediumTextNode.displaysAsynchronously = false
self.highTextNode = TextNode()
self.highTextNode.isUserInteractionEnabled = false
self.highTextNode.displaysAsynchronously = false
self.unlimitedTextNode = TextNode()
self.unlimitedTextNode.isUserInteractionEnabled = false
self.unlimitedTextNode.displaysAsynchronously = false
self.customTextNode = TextNode()
self.customTextNode.isUserInteractionEnabled = false
self.customTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lowTextNode)
self.addSubnode(self.mediumTextNode)
self.addSubnode(self.highTextNode)
self.addSubnode(self.unlimitedTextNode)
self.addSubnode(self.customTextNode)
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
if case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.value = CGFloat(item.value.position)
sliderView.isUserInteractionEnabled = item.enabled
sliderView.alpha = item.enabled ? 1.0 : 0.4
sliderView.layer.allowsGroupOpacity = !item.enabled
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value.position)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.startColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
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: ItemListInviteLinkTimeLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeLowTextLayout = TextNode.asyncLayout(self.lowTextNode)
let makeMediumTextLayout = TextNode.asyncLayout(self.mediumTextNode)
let makeHighTextLayout = TextNode.asyncLayout(self.highTextNode)
let makeUnlimitedTextLayout = TextNode.asyncLayout(self.unlimitedTextNode)
let makeCustomTextLayout = TextNode.asyncLayout(self.customTextNode)
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 (lowTextLayout, lowTextApply) = makeLowTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 3600), 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()))
let (mediumTextLayout, mediumTextApply) = makeMediumTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 86400), 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()))
let (highTextLayout, highTextApply) = makeHighTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 604800), 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()))
let (unlimitedTextLayout, unlimitedTextApply) = makeUnlimitedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.InviteLink_Create_TimeLimitNoLimit, 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()))
let customTextString: String
if case let .custom(value) = item.value {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let relativeValue = value - currentTime
if relativeValue > 0 {
customTextString = timeIntervalString(strings: item.strings, value: relativeValue)
} else {
customTextString = ""
}
} else {
customTextString = ""
}
let (customTextLayout, customTextApply) = makeCustomTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: customTextString, 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()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors)
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 //params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.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 _ = lowTextApply()
let _ = mediumTextApply()
let _ = highTextApply()
let _ = unlimitedTextApply()
let _ = customTextApply()
var textNodes: [(TextNode, CGSize)] = [(strongSelf.lowTextNode, lowTextLayout.size),
(strongSelf.mediumTextNode, mediumTextLayout.size),
(strongSelf.highTextNode, highTextLayout.size),
(strongSelf.unlimitedTextNode, unlimitedTextLayout.size)]
if case .custom = item.value {
textNodes.insert((strongSelf.customTextNode, customTextLayout.size), at: item.value.position)
}
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.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
}
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, short: Bool) {
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 else {
return
}
let position = Int(sliderView.value)
let value = InviteLinkTimeLimit(position: position)
self.item?.updated(value)
}
}

View file

@ -0,0 +1,253 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import SyncCore
import TelegramPresentationData
import ItemListUI
public class ItemListInviteLinkGridItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let invites: [ExportedInvitation]?
public let sectionId: ItemListSectionId
let style: ItemListStyle
let tapAction: ((ExportedInvitation) -> Void)?
let contextAction: ((ExportedInvitation, ASDisplayNode) -> Void)?
public let tag: ItemListItemTag?
public init(
presentationData: ItemListPresentationData,
invites: [ExportedInvitation]?,
sectionId: ItemListSectionId,
style: ItemListStyle,
tapAction: ((ExportedInvitation) -> Void)?,
contextAction: ((ExportedInvitation, ASDisplayNode) -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.invites = invites
self.sectionId = sectionId
self.style = style
self.tapAction = tapAction
self.contextAction = contextAction
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 = ItemListInviteLinkGridItemNode()
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? ItemListInviteLinkGridItemNode {
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()
})
}
}
}
}
}
public var selectable: Bool = false
public func selected(listView: ListView){
}
}
public class ItemListInviteLinkGridItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let gridNode: InviteLinksGridNode
private var item: ItemListInviteLinkGridItem?
override public var canBeSelected: Bool {
return false
}
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.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.gridNode = InviteLinksGridNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.gridNode)
}
public func asyncLayout() -> (_ item: ItemListInviteLinkGridItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
var height: CGFloat
let count = item.invites?.count ?? 0
if count > 0 {
if count % 2 == 0 {
height = 4.0 + 122.0 + 6.0
} else {
height = 4.0 + 102.0 + 6.0
}
} else {
height = 0.001
}
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)
}
if case .sameSection(false) = neighbors.bottom {
} else {
height += 6.0
}
contentSize = CGSize(width: params.width, height: height)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [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 gridSize = strongSelf.gridNode.update(size: contentSize, safeInset: params.leftInset, items: item.invites ?? [], presentationData: item.presentationData, transition: .immediate)
strongSelf.gridNode.frame = CGRect(origin: CGPoint(), size: gridSize)
strongSelf.gridNode.action = { invite in
item.tapAction?(invite)
}
strongSelf.gridNode.contextAction = { node, invite in
item.contextAction?(invite, node)
}
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, 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
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = true
default:
bottomStripeInset = 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: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
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)
}
}

View file

@ -0,0 +1,415 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
enum InviteLinkUsageLimit: Equatable {
case low
case medium
case high
case unlimited
case custom(Int32)
init(position: Int) {
switch position {
case 0:
self = .low
case 1:
self = .medium
case 2:
self = .high
default:
self = .unlimited
}
}
init(value: Int32?) {
if let value = value {
if value == 1 {
self = .low
} else if value == 10 {
self = .medium
} else if value == 100 {
self = .high
} else {
self = .custom(value)
}
} else {
self = .unlimited
}
}
var value: Int32? {
switch self {
case .low:
return 1
case .medium:
return 10
case .high:
return 100
case .unlimited:
return nil
case let .custom(value):
return value
}
}
var position: Int {
switch self {
case .low:
return 0
case .medium:
return 1
case .high:
return 2
case .unlimited:
return 3
case let .custom(value):
if value < 10 {
return 1
} else if value < 100 {
return 2
} else {
return 3
}
}
}
}
final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: InviteLinkUsageLimit
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (InviteLinkUsageLimit) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.enabled = enabled
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 = ItemListInviteLinkUsageLimitItemNode()
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? ItemListInviteLinkUsageLimitItemNode {
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 generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
private final class ItemListInviteLinkUsageLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lowTextNode: TextNode
private let mediumTextNode: TextNode
private let highTextNode: TextNode
private let unlimitedTextNode: TextNode
private let customTextNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: ItemListInviteLinkUsageLimitItem?
private var layoutParams: ListViewItemLayoutParams?
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.lowTextNode = TextNode()
self.lowTextNode.isUserInteractionEnabled = false
self.lowTextNode.displaysAsynchronously = false
self.mediumTextNode = TextNode()
self.mediumTextNode.isUserInteractionEnabled = false
self.mediumTextNode.displaysAsynchronously = false
self.highTextNode = TextNode()
self.highTextNode.isUserInteractionEnabled = false
self.highTextNode.displaysAsynchronously = false
self.unlimitedTextNode = TextNode()
self.unlimitedTextNode.isUserInteractionEnabled = false
self.unlimitedTextNode.displaysAsynchronously = false
self.customTextNode = TextNode()
self.customTextNode.isUserInteractionEnabled = false
self.customTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lowTextNode)
self.addSubnode(self.mediumTextNode)
self.addSubnode(self.highTextNode)
self.addSubnode(self.unlimitedTextNode)
self.addSubnode(self.customTextNode)
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
if case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.value = CGFloat(item.value.position)
sliderView.isUserInteractionEnabled = item.enabled
sliderView.alpha = item.enabled ? 1.0 : 0.4
sliderView.layer.allowsGroupOpacity = !item.enabled
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value.position)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.startColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
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: ItemListInviteLinkUsageLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeLowTextLayout = TextNode.asyncLayout(self.lowTextNode)
let makeMediumTextLayout = TextNode.asyncLayout(self.mediumTextNode)
let makeHighTextLayout = TextNode.asyncLayout(self.highTextNode)
let makeUnlimitedTextLayout = TextNode.asyncLayout(self.unlimitedTextNode)
let makeCustomTextLayout = TextNode.asyncLayout(self.customTextNode)
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 (lowTextLayout, lowTextApply) = makeLowTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "1", 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()))
let (mediumTextLayout, mediumTextApply) = makeMediumTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "10", 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()))
let (highTextLayout, highTextApply) = makeHighTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "100", 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()))
let (unlimitedTextLayout, unlimitedTextApply) = makeUnlimitedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.InviteLink_Create_UsersLimitNoLimit, 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()))
let customTextString: String
if case let .custom(value) = item.value {
customTextString = "\(value)"
} else {
customTextString = ""
}
let (customTextLayout, customTextApply) = makeCustomTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: customTextString, 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()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors)
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 //params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.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 _ = lowTextApply()
let _ = mediumTextApply()
let _ = highTextApply()
let _ = unlimitedTextApply()
let _ = customTextApply()
var textNodes: [(TextNode, CGSize)] = [(strongSelf.lowTextNode, lowTextLayout.size),
(strongSelf.mediumTextNode, mediumTextLayout.size),
(strongSelf.highTextNode, highTextLayout.size),
(strongSelf.unlimitedTextNode, unlimitedTextLayout.size)]
if case .custom = item.value {
textNodes.insert((strongSelf.customTextNode, customTextLayout.size), at: item.value.position)
}
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.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
}
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, short: Bool) {
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 else {
return
}
let position = Int(sliderView.value)
let value = InviteLinkUsageLimit(position: position)
self.item?.updated(value)
}
}

View file

@ -0,0 +1,451 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import SyncCore
import AccountContext
import TelegramPresentationData
import ItemListUI
import SolidRoundedButtonNode
import AnimatedAvatarSetNode
private func actionButtonImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 4.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 16.0, y: 10.0), size: CGSize(width: 4.0, height: 4.0)))
})
}
public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let invite: ExportedInvitation?
let peers: [Peer]
let buttonColor: UIColor?
public let sectionId: ItemListSectionId
let style: ItemListStyle
let shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode) -> Void)?
let viewAction: (() -> Void)?
public let tag: ItemListItemTag?
public init(
context: AccountContext,
presentationData: ItemListPresentationData,
invite: ExportedInvitation?,
peers: [Peer],
buttonColor: UIColor?,
sectionId: ItemListSectionId,
style: ItemListStyle,
shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode) -> Void)?,
viewAction: (() -> Void)?,
tag: ItemListItemTag? = nil
) {
self.context = context
self.presentationData = presentationData
self.invite = invite
self.peers = peers
self.buttonColor = buttonColor
self.sectionId = sectionId
self.style = style
self.shareAction = shareAction
self.contextAction = contextAction
self.viewAction = viewAction
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 = ItemListPermanentInviteLinkItemNode()
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? ItemListPermanentInviteLinkItemNode {
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()
})
}
}
}
}
}
public var selectable: Bool = false
}
public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let fieldNode: ASImageNode
private let addressNode: TextNode
private let extractedContainerNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let addressButtonNode: HighlightTrackingButtonNode
private let addressButtonIconNode: ASImageNode
private var shareButtonNode: SolidRoundedButtonNode?
private let avatarsButtonNode: HighlightTrackingButtonNode
private let avatarsContext: AnimatedAvatarSetContext
private var avatarsContent: AnimatedAvatarSetContext.Content?
private let avatarsNode: AnimatedAvatarSetNode
private let invitedPeersNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: ItemListPermanentInviteLinkItem?
override public var canBeSelected: Bool {
return false
}
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.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.fieldNode = ASImageNode()
self.fieldNode.displaysAsynchronously = false
self.fieldNode.displayWithoutProcessing = true
self.addressNode = TextNode()
self.addressNode.isUserInteractionEnabled = false
self.addressButtonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.addressButtonIconNode = ASImageNode()
self.addressButtonIconNode.displaysAsynchronously = false
self.addressButtonIconNode.displayWithoutProcessing = true
self.avatarsButtonNode = HighlightTrackingButtonNode()
self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode()
self.invitedPeersNode = TextNode()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.fieldNode)
self.addSubnode(self.addressNode)
self.addSubnode(self.avatarsNode)
self.addSubnode(self.invitedPeersNode)
self.addSubnode(self.avatarsButtonNode)
self.containerNode.addSubnode(self.extractedContainerNode)
self.extractedContainerNode.contentNode.addSubnode(self.addressButtonIconNode)
self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode
self.addressButtonNode.addSubnode(self.containerNode)
self.addSubnode(self.addressButtonNode)
self.addSubnode(self.activateArea)
self.addressButtonNode.addTarget(self, action: #selector(self.addressButtonPressed), forControlEvents: .touchUpInside)
self.addressButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.addressButtonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.addressButtonIconNode.alpha = 0.4
} else {
strongSelf.addressButtonIconNode.alpha = 1.0
strongSelf.addressButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.shareButtonNode?.pressed = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
item.shareAction?()
}
}
self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.avatarsNode.layer.removeAnimation(forKey: "opacity")
strongSelf.invitedPeersNode.layer.removeAnimation(forKey: "opacity")
strongSelf.avatarsNode.alpha = 0.4
strongSelf.invitedPeersNode.alpha = 0.4
} else {
strongSelf.avatarsNode.alpha = 1.0
strongSelf.invitedPeersNode.alpha = 1.0
strongSelf.avatarsNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.invitedPeersNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.avatarsButtonNode.addTarget(self, action: #selector(self.avatarsButtonPressed), forControlEvents: .touchUpInside)
}
@objc private func addressButtonPressed() {
if let item = self.item {
item.contextAction?(self.extractedContainerNode)
}
}
@objc private func avatarsButtonPressed() {
if let item = self.item {
item.viewAction?()
}
}
public func asyncLayout() -> (_ item: ItemListPermanentInviteLinkItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
let makeInvitedPeersLayout = TextNode.asyncLayout(self.invitedPeersNode)
let currentItem = self.item
let avatarsContext = self.avatarsContext
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let titleColor: UIColor
titleColor = item.presentationData.theme.list.itemPrimaryTextColor
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invite.flatMap({ $0.link.replacingOccurrences(of: "https://", with: "") }) ?? "", font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitle: String
let subtitleColor: UIColor
if let count = item.invite?.count {
if count > 0 {
subtitle = item.presentationData.strings.InviteLink_PeopleJoined(count)
subtitleColor = item.presentationData.theme.list.itemAccentColor
} else {
subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone
subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor
}
} else {
subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone
subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor
}
let (invitedPeersLayout, invitedPeersApply) = makeInvitedPeersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let avatarsContent = avatarsContext.update(peers: item.peers, animated: false)
let verticalInset: CGFloat = 16.0
let fieldHeight: CGFloat = 52.0
let fieldSpacing: CGFloat = 16.0
let buttonHeight: CGFloat = 50.0
var height = verticalInset * 2.0 + fieldHeight + fieldSpacing + buttonHeight + 54.0
switch item.style {
case .plain:
height -= 57.0
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear
insets = UIEdgeInsets()
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors)
}
contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.avatarsContent = avatarsContent
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.label
strongSelf.activateArea.accessibilityTraits = []
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.fieldNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: item.presentationData.theme.list.freePlainInputField.backgroundColor)
strongSelf.addressButtonIconNode.image = actionButtonImage(color: item.presentationData.theme.list.freePlainInputField.controlColor)
}
let _ = addressApply()
let _ = invitedPeersApply()
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, 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
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 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: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight))
strongSelf.fieldNode.frame = fieldFrame
strongSelf.addressNode.frame = CGRect(origin: CGPoint(x: fieldFrame.minX + floorToScreenPixels((fieldFrame.width - addressLayout.size.width) / 2.0), y: fieldFrame.minY + floorToScreenPixels((fieldFrame.height - addressLayout.size.height) / 2.0) + 1.0), size: addressLayout.size)
strongSelf.addressButtonNode.frame = CGRect(origin: CGPoint(x: params.width - rightInset - 38.0, y: verticalInset + 14.0), size: CGSize(width: 24.0, height: 24.0))
strongSelf.extractedContainerNode.frame = strongSelf.addressButtonNode.bounds
strongSelf.extractedContainerNode.contentRect = strongSelf.addressButtonNode.bounds
strongSelf.addressButtonIconNode.frame = strongSelf.addressButtonNode.bounds
let shareButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.shareButtonNode {
shareButtonNode = currentShareButtonNode
} else {
let buttonTheme: SolidRoundedButtonTheme
if let buttonColor = item.buttonColor {
buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
} else {
buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
}
shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 10.0)
shareButtonNode.title = item.presentationData.strings.InviteLink_Share
shareButtonNode.pressed = {
item.shareAction?()
}
strongSelf.addSubnode(shareButtonNode)
strongSelf.shareButtonNode = shareButtonNode
}
let buttonWidth = contentSize.width - leftInset - rightInset
let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight)
var totalWidth = invitedPeersLayout.size.width
var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0)
let avatarSpacing: CGFloat = 21.0
if let avatarsContent = strongSelf.avatarsContent {
let avatarsSize = strongSelf.avatarsNode.update(context: item.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true)
if !avatarsSize.width.isZero {
totalWidth += avatarsSize.width + avatarSpacing
}
let avatarsNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0), size: avatarsSize)
strongSelf.avatarsNode.frame = avatarsNodeFrame
if !avatarsSize.width.isZero {
leftOrigin = avatarsNodeFrame.maxX + avatarSpacing
}
}
strongSelf.invitedPeersNode.frame = CGRect(origin: CGPoint(x: leftOrigin, y: fieldFrame.maxY + 92.0), size: invitedPeersLayout.size)
strongSelf.avatarsButtonNode.frame = CGRect(x: floorToScreenPixels((params.width - totalWidth) / 2.0), y: fieldFrame.maxY + 87.0, width: totalWidth, height: 32.0)
strongSelf.avatarsButtonNode.isUserInteractionEnabled = !item.peers.isEmpty
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
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)
}
}

View file

@ -22,17 +22,19 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let icon: UIImage?
let title: String
public let alwaysPlain: Bool
let hasSeparator: Bool
let editing: Bool
let height: ItemListPeerActionItemHeight
let color: ItemListPeerActionItemColor
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.title = title
self.alwaysPlain = alwaysPlain
self.hasSeparator = hasSeparator
self.editing = editing
self.height = height
self.color = color
@ -257,6 +259,8 @@ class ItemListPeerActionItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.bottomStripeNode.isHidden = strongSelf.bottomStripeNode.isHidden || !item.hasSeparator
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)))

View file

@ -29,6 +29,11 @@ public enum ItemListSingleLineInputClearType: Equatable {
}
}
public enum ItemListSingleLineInputAlignment {
case `default`
case right
}
public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let title: NSAttributedString
@ -36,6 +41,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let placeholder: String
let type: ItemListSingleLineInputItemType
let returnKeyType: UIReturnKeyType
let alignment: ItemListSingleLineInputAlignment
let spacing: CGFloat
let clearType: ItemListSingleLineInputClearType
let maxLength: Int
@ -49,13 +55,14 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)?
public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.presentationData = presentationData
self.title = title
self.text = text
self.placeholder = placeholder
self.type = type
self.returnKeyType = returnKeyType
self.alignment = alignment
self.spacing = spacing
self.clearType = clearType
self.maxLength = maxLength
@ -325,6 +332,13 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: 1.0), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: layout.contentSize.height - 2.0))
switch item.alignment {
case .default:
strongSelf.textNode.textField.textAlignment = .natural
case .right:
strongSelf.textNode.textField.textAlignment = .right
}
if let image = updatedClearIcon {
strongSelf.clearIconNode.image = image
}

View file

@ -16,12 +16,15 @@
@property (nonatomic, copy) void (^didTransitionOut)();
@property (nonatomic, copy) UIView *(^referenceViewForItem)(TGClipboardGalleryPhotoItem *);
@property (nonatomic, copy) void (^completeWithItem)(TGClipboardGalleryPhotoItem *item);
@property (nonatomic, copy) void (^completeWithItem)(TGClipboardGalleryPhotoItem *item, bool silentPosting, int32_t scheduleTime);
@property (nonatomic, copy) void (^editorOpened)(void);
@property (nonatomic, copy) void (^editorClosed)(void);
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName;
@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t));
@property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder recipientName:(NSString *)recipientName;
- (void)present;

View file

@ -14,7 +14,7 @@
@interface TGClipboardMenu : NSObject
+ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id<LegacyComponentsContext>)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id<TGMediaSelectableItem> currentItem))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect;
+ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id<LegacyComponentsContext>)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder recipientName:(NSString *)recipientName suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext presentScheduleController:(void (^)(void(^)(int32_t)))presentScheduleController presentTimerController:(void (^)(void(^)(int32_t)))presentTimerController completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id<TGMediaSelectableItem> currentItem, bool silentPosting, int32_t scheduleTime))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect;
+ (NSArray *)resultSignalsForSelectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext currentItem:(id<TGMediaSelectableItem>)currentItem descriptionGenerator:(id (^)(id, NSString *, NSArray *, NSString *))descriptionGenerator;

View file

@ -2,6 +2,8 @@
#import <LegacyComponents/LegacyComponents.h>
#import "LegacyComponentsInternal.h"
#import <LegacyComponents/TGModernGalleryController.h>
#import "TGClipboardGalleryPhotoItem.h"
#import "TGClipboardGalleryModel.h"
@ -15,6 +17,8 @@
#import <LegacyComponents/TGMediaAssetMomentList.h>
#import <LegacyComponents/TGMediaAssetMoment.h>
#import "TGMediaPickerSendActionSheetController.h"
@interface TGClipboardGalleryMixin ()
{
TGMediaEditingContext *_editingContext;
@ -35,7 +39,7 @@
@implementation TGClipboardGalleryMixin
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context image:(UIImage *)image images:(NSArray *)images parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext hasCaptions:(bool)hasCaptions hasTimer:(bool)hasTimer hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder recipientName:(NSString *)recipientName
{
self = [super init];
if (self != nil)
@ -109,7 +113,96 @@
strongSelf->_galleryModel.dismiss(true, false);
if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item);
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item, false, 0);
};
model.interfaceView.doneLongPressed = ^(id<TGModernGalleryItem> item) {
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil || !(hasSilentPosting || hasSchedule))
return;
if (iosMajorVersion() >= 10) {
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[generator impactOccurred];
}
bool effectiveHasSchedule = hasSchedule;
for (id item in strongSelf->_galleryModel.selectionContext.selectedItems)
{
if ([item isKindOfClass:[TGMediaAsset class]])
{
if ([[strongSelf->_editingContext timerForItem:item] integerValue] > 0)
{
effectiveHasSchedule = false;
break;
}
}
}
TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSchedule:effectiveHasSchedule reminder:reminder hasTimer:hasTimer];
controller.send = ^{
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_galleryModel.dismiss(true, false);
if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item, false, 0);
};
controller.sendSilently = ^{
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_galleryModel.dismiss(true, false);
if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item, true, 0);
};
controller.schedule = ^{
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf.presentScheduleController(^(int32_t time) {
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_galleryModel.dismiss(true, false);
if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item, false, time);
});
};
controller.sendWithTimer = ^{
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf.presentTimerController(^(int32_t time) {
__strong TGClipboardGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_galleryModel.dismiss(true, false);
TGMediaEditingContext *editingContext = strongSelf->_editingContext;
NSMutableArray *items = [strongSelf->_galleryModel.selectionContext.selectedItems mutableCopy];
[items addObject:((TGClipboardGalleryPhotoItem *)item).image];
for (id<TGMediaEditableItem> editableItem in items) {
[editingContext setTimer:@(time) forItem:editableItem];
}
if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem((TGClipboardGalleryPhotoItem *)item, false, 0);
});
};
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[strongSelf->_context makeOverlayWindowManager] parentController:strongSelf->_parentController contentController:controller];
controllerWindow.hidden = false;
};
modernGallery.model = model;

View file

@ -9,7 +9,7 @@
@implementation TGClipboardMenu
+ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id<LegacyComponentsContext>)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer recipientName:(NSString *)recipientName suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id<TGMediaSelectableItem> currentItem))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect
+ (TGMenuSheetController *)presentInParentController:(TGViewController *)parentController context:(id<LegacyComponentsContext>)context images:(NSArray *)images hasCaption:(bool)hasCaption hasTimer:(bool)hasTimer hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder recipientName:(NSString *)recipientName suggestionContext:(TGSuggestionContext *)suggestionContext stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext presentScheduleController:(void (^)(void(^)(int32_t)))presentScheduleController presentTimerController:(void (^)(void(^)(int32_t)))presentTimerController completed:(void (^)(TGMediaSelectionContext *selectionContext, TGMediaEditingContext *editingContext, id<TGMediaSelectableItem> currentItem, bool silentPosting, int32_t scheduleTime))completed dismissed:(void (^)(void))dismissed sourceView:(UIView *)sourceView sourceRect:(CGRect (^)(void))sourceRect
{
bool centered = false;
if (sourceRect == nil)
@ -46,11 +46,16 @@
previewItem.parentController = parentController;
previewItem.allowCaptions = hasCaption;
previewItem.hasTimer = hasTimer;
previewItem.hasSilentPosting = hasSilentPosting;
previewItem.hasSchedule = hasSchedule;
previewItem.reminder = reminder;
previewItem.recipientName = recipientName;
previewItem.sendPressed = ^(UIImage *currentItem)
previewItem.presentScheduleController = presentScheduleController;
previewItem.presentTimerController = presentTimerController;
previewItem.sendPressed = ^(UIImage *currentItem, bool silentPosting, int32_t scheduleTime)
{
__strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem;
completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, currentItem);
completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, currentItem, silentPosting, scheduleTime);
__strong TGMenuSheetController *strongController = weakController;
[strongController dismissAnimated:true];
@ -68,7 +73,7 @@
TGMenuSheetButtonItemView *sendItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:sendTitle type:TGMenuSheetButtonTypeSend fontSize:20.0 action:^
{
__strong TGClipboardPreviewItemView *strongPreviewItem = weakPreviewItem;
completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, nil);
completed(strongPreviewItem.selectionContext, strongPreviewItem.editingContext, nil, false, 0);
__strong TGMenuSheetController *strongController = weakController;
[strongController dismissAnimated:true];

View file

@ -14,13 +14,20 @@
@property (nonatomic, assign) bool hasTimer;
@property (nonatomic, strong) NSString *recipientName;
@property (nonatomic, assign) bool hasSchedule;
@property (nonatomic, assign) bool hasSilentPosting;
@property (nonatomic, assign) bool reminder;
@property (nonatomic, readonly) TGMediaSelectionContext *selectionContext;
@property (nonatomic, readonly) TGMediaEditingContext *editingContext;
@property (nonatomic, strong) TGSuggestionContext *suggestionContext;
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
@property (nonatomic, copy) void (^selectionChanged)(NSUInteger);
@property (nonatomic, copy) void (^sendPressed)(UIImage *currentItem);
@property (nonatomic, copy) void (^sendPressed)(UIImage *currentItem, bool silentPosting, int32_t scheduleTime);
@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t));
@property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context images:(NSArray *)images;

View file

@ -248,12 +248,13 @@ const CGFloat TGClipboardPreviewEdgeInset = 8.0f;
strongSelf->_galleryMixin = nil;
};
mixin.completeWithItem = ^(TGClipboardGalleryPhotoItem *item)
mixin.completeWithItem = ^(TGClipboardGalleryPhotoItem *item, bool silentPosting, int32_t scheduleTime)
{
__strong TGClipboardPreviewItemView *strongSelf = weakSelf;
if (strongSelf != nil && strongSelf.sendPressed != nil)
strongSelf.sendPressed(item.image);
strongSelf.sendPressed(item.image, silentPosting, scheduleTime);
};
}
@ -266,7 +267,9 @@ const CGFloat TGClipboardPreviewEdgeInset = 8.0f;
if ([cell isKindOfClass:[TGClipboardPreviewCell class]])
thumbnailImage = cell.imageView.image;
TGClipboardGalleryMixin *mixin = [[TGClipboardGalleryMixin alloc] initWithContext:_context image:image images:_images parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext stickersContext:self.stickersContext hasCaptions:self.allowCaptions hasTimer:self.hasTimer recipientName:self.recipientName];
TGClipboardGalleryMixin *mixin = [[TGClipboardGalleryMixin alloc] initWithContext:_context image:image images:_images parentController:self.parentController thumbnailImage:thumbnailImage selectionContext:_selectionContext editingContext:_editingContext suggestionContext:self.suggestionContext stickersContext:self.stickersContext hasCaptions:self.allowCaptions hasTimer:self.hasTimer hasSilentPosting:self.hasSilentPosting hasSchedule:self.hasSchedule reminder:self.reminder recipientName:self.recipientName];
mixin.presentScheduleController = self.presentScheduleController;
mixin.presentTimerController = self.presentTimerController;
[self _setupGalleryMixin:mixin];

View file

@ -138,7 +138,9 @@
if (strongSelf == nil)
return;
[strongSelf.window endEditing:true];
[strongSelf.window endEditing:true];
strongSelf->_portraitToolbarView.doneButton.userInteractionEnabled = false;
strongSelf->_landscapeToolbarView.doneButton.userInteractionEnabled = false;
strongSelf->_donePressed(strongSelf->_currentItem);
};
void(^toolbarDoneLongPressed)(id) = ^(id sender)
@ -591,6 +593,8 @@
[_muteButton setImage:muteIcon forState:UIControlStateNormal];
[_muteButton setImage:muteActiveIcon forState:UIControlStateSelected];
[_muteButton setImage:muteActiveIcon forState:UIControlStateSelected | UIControlStateHighlighted];
[self setNeedsLayout];
}
- (TGPhotoEditorTab)currentTabs

View file

@ -383,7 +383,7 @@ public func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuShee
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, saveEditedPhotos: Bool, allowGrouping: Bool, presentationData: PresentationData, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, presentationData: PresentationData, images: [UIImage], presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -420,10 +420,18 @@ public func presentLegacyPasteMenu(context: AccountContext, peer: Peer, chatLoca
completion?(coder.makeData(), animated, view, rect)
})
}
let controller = TGClipboardMenu.present(inParentController: emptyController, context: legacyController.context, images: images, hasCaption: true, hasTimer: hasTimer, recipientName: recipientName, suggestionContext: suggestionContext, stickersContext: paintStickersContext, completed: { selectionContext, editingContext, currentItem in
let controller = TGClipboardMenu.present(inParentController: emptyController, context: legacyController.context, images: images, hasCaption: true, hasTimer: hasTimer, hasSilentPosting: hasSilentPosting, hasSchedule: hasSchedule, reminder: peer.id == context.account.peerId, recipientName: recipientName, suggestionContext: suggestionContext, stickersContext: paintStickersContext, presentScheduleController: { done in
presentSchedulePicker { time in
done?(time)
}
}, presentTimerController: { done in
presentTimerPicker { time in
done?(time)
}
}, completed: { selectionContext, editingContext, currentItem, silentPosting, scheduleTime in
let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: legacyAssetPickerItemGenerator())
sendMessagesWithSignals(signals)
sendMessagesWithSignals(signals, silentPosting, scheduleTime)
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
}, sourceView: emptyController.view, sourceRect: nil)!

View file

@ -296,6 +296,48 @@ public func legacyEnqueueGifMessage(account: Account, data: Data) -> Signal<Enqu
} |> runOn(Queue.concurrentDefaultQueue())
}
public func legacyEnqueueVideoMessage(account: Account, data: Data) -> Signal<EnqueueMessage, Void> {
return Signal { subscriber in
if let previewImage = UIImage(data: data) {
let dimensions = previewImage.size
var previewRepresentations: [TelegramMediaImageRepresentation] = []
let thumbnailSize = dimensions.aspectFitted(CGSize(width: 320.0, height: 320.0))
let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)!
if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
let resource = LocalFileMediaResource(fileId: arc4random64())
account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData)
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: []))
}
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
let tempFilePath = NSTemporaryDirectory() + "\(randomId).mp4"
let _ = try? FileManager.default.removeItem(atPath: tempFilePath)
let _ = try? data.write(to: URL(fileURLWithPath: tempFilePath), options: [.atomic])
let resource = LocalFileGifMediaResource(randomId: arc4random64(), path: tempFilePath)
let fileName: String = "video.mp4"
let finalDimensions = TGMediaVideoConverter.dimensions(for: dimensions, adjustments: nil, preset: TGMediaVideoConversionPresetAnimation)
var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.Video(duration: Int(0), size: PixelDimensions(finalDimensions), flags: [.supportsStreaming]))
fileAttributes.append(.FileName(fileName: fileName))
fileAttributes.append(.Animated)
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes)
subscriber.putNext(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil))
subscriber.putCompletion()
} else {
subscriber.putError(Void())
}
return EmptyDisposable
} |> runOn(Queue.concurrentDefaultQueue())
}
public func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signal<[EnqueueMessage], Void> {
return Signal { subscriber in
let disposable = SSignal.combineSignals(signals).start(next: { anyValues in

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData

View file

@ -10,88 +10,10 @@
#import <CommonCrypto/CommonDigest.h>
#define IFPGA_NAMESTRING @"iFPGA"
#define IPHONE_1G_NAMESTRING @"iPhone 1G"
#define IPHONE_3G_NAMESTRING @"iPhone 3G"
#define IPHONE_3GS_NAMESTRING @"iPhone 3GS"
#define IPHONE_4_NAMESTRING @"iPhone 4"
#define IPHONE_4S_NAMESTRING @"iPhone 4S"
#define IPHONE_5_NAMESTRING @"iPhone 5"
#define IPHONE_5S_NAMESTRING @"iPhone 5S"
#define IPHONE_6_NAMESTRING @"iPhone 6"
#define IPHONE_6Plus_NAMESTRING @"iPhone 6 Plus"
#define IPHONE_6S_NAMESTRING @"iPhone 6S"
#define IPHONE_6SPlus_NAMESTRING @"iPhone 6S Plus"
#define IPHONE_7_NAMESTRING @"iPhone 7"
#define IPHONE_7Plus_NAMESTRING @"iPhone 7 Plus"
#define IPHONE_8_NAMESTRING @"iPhone 8"
#define IPHONE_8Plus_NAMESTRING @"iPhone 8 Plus"
#define IPHONE_X_NAMESTRING @"iPhone X"
#define IPHONE_SE_NAMESTRING @"iPhone SE"
#define IPHONE_SE2_NAMESTRING @"iPhone SE (2nd gen)"
#define IPHONE_XS_NAMESTRING @"iPhone XS"
#define IPHONE_XSMAX_NAMESTRING @"iPhone XS Max"
#define IPHONE_XR_NAMESTRING @"iPhone XR"
#define IPHONE_11_NAMESTRING @"iPhone 11"
#define IPHONE_11PRO_NAMESTRING @"iPhone 11 Pro"
#define IPHONE_11PROMAX_NAMESTRING @"iPhone 11 Pro Max"
#define IPHONE_12MINI_NAMESTRING @"iPhone 12 mini"
#define IPHONE_12_NAMESTRING @"iPhone 12"
#define IPHONE_12PRO_NAMESTRING @"iPhone 12 Pro"
#define IPHONE_12PROMAX_NAMESTRING @"iPhone 12 Pro Max"
#define IPHONE_UNKNOWN_NAMESTRING @"Unknown iPhone"
#define IPOD_1G_NAMESTRING @"iPod touch 1G"
#define IPOD_2G_NAMESTRING @"iPod touch 2G"
#define IPOD_3G_NAMESTRING @"iPod touch 3G"
#define IPOD_4G_NAMESTRING @"iPod touch 4G"
#define IPOD_5G_NAMESTRING @"iPod touch 5G"
#define IPOD_6G_NAMESTRING @"iPod touch 6G"
#define IPOD_7G_NAMESTRING @"iPod touch 7G"
#define IPOD_UNKNOWN_NAMESTRING @"Unknown iPod"
#define IPAD_1G_NAMESTRING @"iPad 1G"
#define IPAD_2G_NAMESTRING @"iPad 2G"
#define IPAD_3G_NAMESTRING @"iPad 3G"
#define IPAD_4G_NAMESTRING @"iPad 4G"
#define IPAD_5G_NAMESTRING @"iPad Air 2"
#define IPAD_6G_NAMESTRING @"iPad Pro"
#define IPAD_PRO_3G_NAMESTRING @"iPad Pro 12.9 inch (3rd gen)"
#define IPAD_PRO_11_NAMESTRING @"iPad Pro 11 inch"
#define IPAD_PRO_6G_NAMESTRING @"iPad (6th gen)"
#define IPAD_PRO_10_5_NAMESTRING @"iPad Pro 10.5 inch"
#define IPAD_PRO_12_9_NAMESTRING @"iPad Pro 12.9 inch"
#define IPAD_UNKNOWN_NAMESTRING @"Unknown iPad"
#define APPLETV_2G_NAMESTRING @"Apple TV 2G"
#define APPLETV_3G_NAMESTRING @"Apple TV 3G"
#define APPLETV_4G_NAMESTRING @"Apple TV 4G"
#define APPLETV_UNKNOWN_NAMESTRING @"Unknown Apple TV"
#define IOS_FAMILY_UNKNOWN_DEVICE @"Unknown iOS device"
#define SIMULATOR_NAMESTRING @"iPhone Simulator"
#define SIMULATOR_IPHONE_NAMESTRING @"iPhone Simulator"
#define SIMULATOR_IPAD_NAMESTRING @"iPad Simulator"
#define SIMULATOR_APPLETV_NAMESTRING @"Apple TV Simulator"
/*
iPad8,5, iPad8,6, iPad8,7, iPad8,8 - iPad Pro 12.9" (3rd gen)
iPad8,1, iPad8,2, iPad8,3, iPad8,4 - iPad Pro 11"
iPad7,5, iPad7,6 - iPad 6th gen
iPad7,3, iPad7,4 - iPad Pro 10.5"
iPad7,1, iPad7,2 - iPad Pro 12.9" (2ng gen)
*/
typedef enum {
UIDeviceUnknown,
UIDeviceSimulator,
UIDeviceSimulatoriPhone,
UIDeviceSimulatoriPad,
UIDeviceSimulatorAppleTV,
UIDevice1GiPhone,
UIDevice3GiPhone,
@ -157,15 +79,6 @@ typedef enum {
} UIDevicePlatform;
typedef enum {
UIDeviceFamilyiPhone,
UIDeviceFamilyiPod,
UIDeviceFamilyiPad,
UIDeviceFamilyAppleTV,
UIDeviceFamilyUnknown,
} UIDeviceFamily;
static NSData * _Nullable parseHexString(NSString * _Nonnull hex) {
if ([hex length] % 2 != 0) {
return nil;
@ -587,79 +500,213 @@ NSString *suffix = @"";
- (NSString *)platformString
{
switch ([self platformType])
{
case UIDevice1GiPhone: return IPHONE_1G_NAMESTRING;
case UIDevice3GiPhone: return IPHONE_3G_NAMESTRING;
case UIDevice3GSiPhone: return IPHONE_3GS_NAMESTRING;
case UIDevice4iPhone: return IPHONE_4_NAMESTRING;
case UIDevice4SiPhone: return IPHONE_4S_NAMESTRING;
case UIDevice5iPhone: return IPHONE_5_NAMESTRING;
case UIDevice5SiPhone: return IPHONE_5S_NAMESTRING;
case UIDevice6iPhone: return IPHONE_6_NAMESTRING;
case UIDevice6PlusiPhone: return IPHONE_6Plus_NAMESTRING;
case UIDevice6SiPhone: return IPHONE_6S_NAMESTRING;
case UIDevice6SPlusiPhone: return IPHONE_6SPlus_NAMESTRING;
case UIDevice7iPhone: return IPHONE_7_NAMESTRING;
case UIDevice7PlusiPhone: return IPHONE_7Plus_NAMESTRING;
case UIDevice8iPhone: return IPHONE_8_NAMESTRING;
case UIDevice8PlusiPhone: return IPHONE_8Plus_NAMESTRING;
case UIDeviceXiPhone: return IPHONE_X_NAMESTRING;
case UIDeviceSEPhone: return IPHONE_SE_NAMESTRING;
case UIDeviceSE2Phone: return IPHONE_SE2_NAMESTRING;
case UIDeviceXSiPhone: return IPHONE_XS_NAMESTRING;
case UIDeviceXSMaxiPhone: return IPHONE_XSMAX_NAMESTRING;
case UIDeviceXRiPhone: return IPHONE_XR_NAMESTRING;
case UIDevice11iPhone: return IPHONE_11_NAMESTRING;
case UIDevice11ProiPhone: return IPHONE_11PRO_NAMESTRING;
case UIDevice11ProMaxiPhone: return IPHONE_11PROMAX_NAMESTRING;
case UIDevice12MiniiPhone: return IPHONE_12MINI_NAMESTRING;
case UIDevice12iPhone: return IPHONE_12_NAMESTRING;
case UIDevice12ProiPhone: return IPHONE_12PRO_NAMESTRING;
case UIDevice12ProMaxiPhone: return IPHONE_12PROMAX_NAMESTRING;
case UIDeviceUnknowniPhone: return IPHONE_UNKNOWN_NAMESTRING;
case UIDevice1GiPod: return IPOD_1G_NAMESTRING;
case UIDevice2GiPod: return IPOD_2G_NAMESTRING;
case UIDevice3GiPod: return IPOD_3G_NAMESTRING;
case UIDevice4GiPod: return IPOD_4G_NAMESTRING;
case UIDevice5GiPod: return IPOD_5G_NAMESTRING;
case UIDevice6GiPod: return IPOD_6G_NAMESTRING;
case UIDevice7GiPod: return IPOD_7G_NAMESTRING;
case UIDeviceUnknowniPod: return IPOD_UNKNOWN_NAMESTRING;
case UIDevice1GiPad : return IPAD_1G_NAMESTRING;
case UIDevice2GiPad : return IPAD_2G_NAMESTRING;
case UIDevice3GiPad : return IPAD_3G_NAMESTRING;
case UIDevice4GiPad : return IPAD_4G_NAMESTRING;
case UIDevice5GiPad : return IPAD_5G_NAMESTRING;
case UIDevice6GiPad : return IPAD_6G_NAMESTRING;
case UIDeviceiPadPro12_93g : return IPAD_PRO_12_9_NAMESTRING;
case UIDeviceiPadPro11 : return IPAD_PRO_11_NAMESTRING;
case UIDeviceiPadPro6g : return IPAD_PRO_6G_NAMESTRING;
case UIDeviceiPadPro10_5 : return IPAD_PRO_10_5_NAMESTRING;
case UIDeviceiPadPro12_9 : return IPAD_PRO_12_9_NAMESTRING;
case UIDeviceUnknowniPad : return IPAD_UNKNOWN_NAMESTRING;
case UIDeviceAppleTV2 : return APPLETV_2G_NAMESTRING;
case UIDeviceAppleTV3 : return APPLETV_3G_NAMESTRING;
case UIDeviceAppleTV4 : return APPLETV_4G_NAMESTRING;
case UIDeviceUnknownAppleTV: return APPLETV_UNKNOWN_NAMESTRING;
case UIDeviceSimulator: return SIMULATOR_NAMESTRING;
case UIDeviceSimulatoriPhone: return SIMULATOR_IPHONE_NAMESTRING;
case UIDeviceSimulatoriPad: return SIMULATOR_IPAD_NAMESTRING;
case UIDeviceSimulatorAppleTV: return SIMULATOR_APPLETV_NAMESTRING;
case UIDeviceIFPGA: return IFPGA_NAMESTRING;
case UIDeviceOSX: return [self macHWName];
#if TARGET_OS_IPHONE
NSString *platform = [self platform];
if ([platform isEqualToString:@"iPhone1,1"])
return @"iPhone";
if ([platform isEqualToString:@"iPhone1,2"])
return @"iPhone 3G";
if ([platform isEqualToString:@"iPhone2,1"])
return @"iPhone 3GS";
if ([platform hasPrefix:@"iPhone3"])
return @"iPhone 4";
if ([platform hasPrefix:@"iPhone4"])
return @"iPhone 4S";
if ([platform isEqualToString:@"iPhone5,1"] ||
[platform isEqualToString:@"iPhone5,2"])
return @"iPhone 5";
if ([platform isEqualToString:@"iPhone5,3"] ||
[platform isEqualToString:@"iPhone5,4"])
return @"iPhone 5C";
if ([platform hasPrefix:@"iPhone6"])
return @"iPhone 5S";
if ([platform isEqualToString:@"iPhone7,1"])
return @"iPhone 6 Plus";
if ([platform isEqualToString:@"iPhone7,2"])
return @"iPhone 6";
if ([platform isEqualToString:@"iPhone8,1"])
return @"iPhone 6S";
if ([platform isEqualToString:@"iPhone8,2"])
return @"iPhone 6S Plus";
if ([platform isEqualToString:@"iPhone8,4"])
return @"iPhone SE";
if ([platform isEqualToString:@"iPhone9,1"] ||
[platform isEqualToString:@"iPhone9,3"])
return @"iPhone 7";
if ([platform isEqualToString:@"iPhone9,2"] ||
[platform isEqualToString:@"iPhone9,4"])
return @"iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone10,1"] ||
[platform isEqualToString:@"iPhone10,4"])
return @"iPhone 8";
if ([platform isEqualToString:@"iPhone10,2"] ||
[platform isEqualToString:@"iPhone10,5"])
return @"iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,3"] ||
[platform isEqualToString:@"iPhone10,6"])
return @"iPhone X";
if ([platform isEqualToString:@"iPhone11,2"])
return @"iPhone XS";
if ([platform isEqualToString:@"iPhone11,4"] ||
[platform isEqualToString:@"iPhone11,6"])
return @"iPhone XS Max";
if ([platform isEqualToString:@"iPhone11,8"])
return @"iPhone XR";
if ([platform isEqualToString:@"iPhone12,1"])
return @"iPhone 11";
if ([platform isEqualToString:@"iPhone12,3"])
return @"iPhone 11 Pro";
if ([platform isEqualToString:@"iPhone12,5"])
return @"iPhone 11 Pro Max";
if ([platform isEqualToString:@"iPhone12,8"])
return @"iPhone SE (2nd gen)";
if ([platform isEqualToString:@"iPhone13,1"])
return @"iPhone 12 mini";
if ([platform isEqualToString:@"iPhone13,2"])
return @"iPhone 12";
if ([platform isEqualToString:@"iPhone13,3"])
return @"iPhone 12 Pro";
if ([platform isEqualToString:@"iPhone13,4"])
return @"iPhone 12 Pro Max";
if ([platform hasPrefix:@"iPod1"])
return @"iPod touch 1G";
if ([platform hasPrefix:@"iPod2"])
return @"iPod touch 2G";
if ([platform hasPrefix:@"iPod3"])
return @"iPod touch 3G";
if ([platform hasPrefix:@"iPod4"])
return @"iPod touch 4G";
if ([platform hasPrefix:@"iPod5"])
return @"iPod touch 5G";
if ([platform hasPrefix:@"iPod7"])
return @"iPod touch 6G";
if ([platform hasPrefix:@"iPod9"])
return @"iPod touch 7G";
if ([platform isEqualToString:@"iPad2,5"] ||
[platform isEqualToString:@"iPad2,6"] ||
[platform isEqualToString:@"iPad2,7"])
return @"iPad mini";
if ([platform hasPrefix:@"iPad2"])
return @"iPad 2G";
if ([platform isEqualToString:@"iPad3,1"] ||
[platform isEqualToString:@"iPad3,2"] ||
[platform isEqualToString:@"iPad3,3"])
return @"iPad 3G";
if ([platform isEqualToString:@"iPad3,4"] ||
[platform isEqualToString:@"iPad3,5"] ||
[platform isEqualToString:@"iPad3,6"])
return @"iPad 3G";
if ([platform isEqualToString:@"iPad4,1"] ||
[platform isEqualToString:@"iPad4,2"])
return @"iPad Air";
default: return IOS_FAMILY_UNKNOWN_DEVICE;
if ([platform isEqualToString:@"iPad4,4"] ||
[platform isEqualToString:@"iPad4,5"] ||
[platform isEqualToString:@"iPad4,6"])
return @"iPad mini Retina";
if ([platform isEqualToString:@"iPad4,7"] ||
[platform isEqualToString:@"iPad4,8"] ||
[platform isEqualToString:@"iPad4,9"])
return @"iPad mini 3";
if ([platform isEqualToString:@"iPad5,1"] ||
[platform isEqualToString:@"iPad5,2"])
return @"iPad mini 4";
if ([platform isEqualToString:@"iPad5,3"] ||
[platform isEqualToString:@"iPad5,4"])
return @"iPad Air 2";
if ([platform isEqualToString:@"iPad6,3"] ||
[platform isEqualToString:@"iPad6,4"])
return @"iPad Pro 9.7 inch";
if ([platform isEqualToString:@"iPad6,7"] ||
[platform isEqualToString:@"iPad6,8"])
return @"iPad Pro 12.9 inch";
if ([platform isEqualToString:@"iPad6,11"] ||
[platform isEqualToString:@"iPad6,12"])
return @"iPad (2017)";
if ([platform isEqualToString:@"iPad7,1"] ||
[platform isEqualToString:@"iPad7,2"])
return @"iPad Pro (2nd gen)";
if ([platform isEqualToString:@"iPad7,3"] ||
[platform isEqualToString:@"iPad7,4"])
return @"iPad Pro 10.5 inch";
if ([platform isEqualToString:@"iPad7,5"] ||
[platform isEqualToString:@"iPad7,6"])
return @"iPad (6th gen)";
if ([platform isEqualToString:@"iPad7,11"] ||
[platform isEqualToString:@"iPad7,12"])
return @"iPad 10.2 inch (7th gen)";
if ([platform isEqualToString:@"iPad8,1"] ||
[platform isEqualToString:@"iPad8,2"] ||
[platform isEqualToString:@"iPad8,3"] ||
[platform isEqualToString:@"iPad8,4"])
return @"iPad Pro 11 inch (3rd gen)";
if ([platform isEqualToString:@"iPad8,5"] ||
[platform isEqualToString:@"iPad8,6"] ||
[platform isEqualToString:@"iPad8,7"] ||
[platform isEqualToString:@"iPad8,8"])
return @"iPad Pro 12.9 inch (3rd gen)";
if ([platform isEqualToString:@"iPad8,9"] ||
[platform isEqualToString:@"iPad8,10"])
return @"iPad Pro 11 inch (4th gen)";
if ([platform isEqualToString:@"iPad8,11"] ||
[platform isEqualToString:@"iPad8,12"])
return @"iPad Pro 12.9 inch (4th gen)";
if ([platform isEqualToString:@"iPad11,1"] ||
[platform isEqualToString:@"iPad11,2"])
return @"iPad mini (5th gen)";
if ([platform isEqualToString:@"iPad11,3"] ||
[platform isEqualToString:@"iPad11,4"])
return @"iPad Air (3rd gen)";
if ([platform isEqualToString:@"iPad11,6"] ||
[platform isEqualToString:@"iPad11,7"])
return @"iPad (8th gen)";
if ([platform isEqualToString:@"iPad13,1"] ||
[platform isEqualToString:@"iPad13,2"])
return @"iPad Air (4th gen)";
if ([platform hasPrefix:@"iPhone"])
return @"Unknown iPhone";
if ([platform hasPrefix:@"iPod"])
return @"Unknown iPod";
if ([platform hasPrefix:@"iPad"])
return @"Unknown iPad";
if ([platform hasSuffix:@"86"] || [platform isEqual:@"x86_64"] || [platform isEqual:@"arm64"]) {
return @"iPhone Simulator";
}
#else
return [self macHWName];
#endif
return @"Unknown iOS device";
}
-(NSString *)macHWName {
- (NSString *)macHWName {
size_t len = 0;
sysctlbyname("hw.model", NULL, &len, NULL, 0);
if (len) {
@ -672,120 +719,6 @@ NSString *suffix = @"";
return @"macOS";
}
- (NSUInteger)platformType
{
#if TARGET_OS_IPHONE
NSString *platform = [self platform];
// The ever mysterious iFPGA
if ([platform isEqualToString:@"iFPGA"]) return UIDeviceIFPGA;
// iPhone
if ([platform isEqualToString:@"iPhone1,1"]) return UIDevice1GiPhone;
if ([platform isEqualToString:@"iPhone1,2"]) return UIDevice3GiPhone;
if ([platform hasPrefix:@"iPhone2"]) return UIDevice3GSiPhone;
if ([platform hasPrefix:@"iPhone3"]) return UIDevice4iPhone;
if ([platform hasPrefix:@"iPhone4"]) return UIDevice4SiPhone;
if ([platform hasPrefix:@"iPhone5"]) return UIDevice5iPhone;
if ([platform hasPrefix:@"iPhone6"]) return UIDevice5SiPhone;
if ([platform isEqualToString:@"iPhone7,1"]) return UIDevice6PlusiPhone;
if ([platform isEqualToString:@"iPhone7,2"]) return UIDevice6iPhone;
if ([platform isEqualToString:@"iPhone8,1"]) return UIDevice6SiPhone;
if ([platform isEqualToString:@"iPhone8,2"]) return UIDevice6SPlusiPhone;
if ([platform isEqualToString:@"iPhone9,1"]) return UIDevice7iPhone;
if ([platform isEqualToString:@"iPhone9,3"]) return UIDevice7iPhone;
if ([platform isEqualToString:@"iPhone9,2"]) return UIDevice7PlusiPhone;
if ([platform isEqualToString:@"iPhone9,4"]) return UIDevice7PlusiPhone;
if ([platform isEqualToString:@"iPhone10,1"]) return UIDevice8iPhone;
if ([platform isEqualToString:@"iPhone10,4"]) return UIDevice8iPhone;
if ([platform isEqualToString:@"iPhone10,2"]) return UIDevice8PlusiPhone;
if ([platform isEqualToString:@"iPhone10,5"]) return UIDevice8PlusiPhone;
if ([platform isEqualToString:@"iPhone10,3"]) return UIDeviceXiPhone;
if ([platform isEqualToString:@"iPhone10,6"]) return UIDeviceXiPhone;
if ([platform isEqualToString:@"iPhone11,2"]) return UIDeviceXSiPhone;
if ([platform isEqualToString:@"iPhone11,6"]) return UIDeviceXSMaxiPhone;
if ([platform isEqualToString:@"iPhone11,4"]) return UIDeviceXSMaxiPhone;
if ([platform isEqualToString:@"iPhone11,8"]) return UIDeviceXRiPhone;
if ([platform isEqualToString:@"iPhone12,1"]) return UIDevice11iPhone;
if ([platform isEqualToString:@"iPhone12,3"]) return UIDevice11ProiPhone;
if ([platform isEqualToString:@"iPhone12,5"]) return UIDevice11ProMaxiPhone;
if ([platform isEqualToString:@"iPhone12,8"]) return UIDeviceSE2Phone;
if ([platform isEqualToString:@"iPhone13,1"]) return UIDevice12MiniiPhone;
if ([platform isEqualToString:@"iPhone13,2"]) return UIDevice12iPhone;
if ([platform isEqualToString:@"iPhone13,3"]) return UIDevice12ProiPhone;
if ([platform isEqualToString:@"iPhone13,4"]) return UIDevice12ProMaxiPhone;
if ([platform isEqualToString:@"iPhone8,4"]) return UIDeviceSEPhone;
// iPod
if ([platform hasPrefix:@"iPod1"]) return UIDevice1GiPod;
if ([platform hasPrefix:@"iPod2"]) return UIDevice2GiPod;
if ([platform hasPrefix:@"iPod3"]) return UIDevice3GiPod;
if ([platform hasPrefix:@"iPod4"]) return UIDevice4GiPod;
if ([platform hasPrefix:@"iPod5"]) return UIDevice5GiPod;
if ([platform hasPrefix:@"iPod7"]) return UIDevice6GiPod;
if ([platform hasPrefix:@"iPod9"]) return UIDevice7GiPod;
// iPad
if ([platform hasPrefix:@"iPad1"]) return UIDevice1GiPad;
if ([platform hasPrefix:@"iPad2"]) return UIDevice2GiPad;
if ([platform hasPrefix:@"iPad3"]) return UIDevice3GiPad;
if ([platform hasPrefix:@"iPad4"]) return UIDevice4GiPad;
if ([platform hasPrefix:@"iPad5"]) return UIDevice5GiPad;
if ([platform hasPrefix:@"iPad6"]) return UIDevice6GiPad;
if ([platform isEqualToString:@"iPad8,5"] ||
[platform isEqualToString:@"iPad8,6"] ||
[platform isEqualToString:@"iPad8,7"] ||
[platform isEqualToString:@"iPad8,8"]) {
return UIDeviceiPadPro12_93g;
}
if ([platform isEqualToString:@"iPad8,1"] ||
[platform isEqualToString:@"iPad8,2"] ||
[platform isEqualToString:@"iPad8,3"] ||
[platform isEqualToString:@"iPad8,4"]) {
return UIDeviceiPadPro11;
}
if ([platform isEqualToString:@"iPad7,5"] ||
[platform isEqualToString:@"iPad7,6"]) {
return UIDeviceiPadPro6g;
}
if ([platform isEqualToString:@"iPad7,3"] ||
[platform isEqualToString:@"iPad7,4"]) {
return UIDeviceiPadPro10_5;
}
if ([platform isEqualToString:@"iPad7,1"] ||
[platform isEqualToString:@"iPad7,2"]) {
return UIDeviceiPadPro12_9;
}
// Apple TV
if ([platform hasPrefix:@"AppleTV2"]) return UIDeviceAppleTV2;
if ([platform hasPrefix:@"AppleTV3"]) return UIDeviceAppleTV3;
if ([platform hasPrefix:@"iPhone"]) return UIDeviceUnknowniPhone;
if ([platform hasPrefix:@"iPod"]) return UIDeviceUnknowniPod;
if ([platform hasPrefix:@"iPad"]) return UIDeviceUnknowniPad;
if ([platform hasPrefix:@"AppleTV"]) return UIDeviceUnknownAppleTV;
// Simulator thanks Jordan Breeding
if ([platform hasSuffix:@"86"] || [platform isEqual:@"x86_64"])
{
return UIDeviceSimulatoriPhone;
}
#else
return UIDeviceOSX;
#endif
return UIDeviceUnknown;
}
- (NSString *)getSysInfoByName:(char *)typeSpecifier
{
size_t size;

View file

@ -67,6 +67,7 @@ swift_library(
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/StatisticsUI:StatisticsUI",
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
"//submodules/InviteLinksUI:InviteLinksUI",
],
visibility = [
"//visibility:public",

View file

@ -691,7 +691,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
canTransfer = true
}
if let initialParticipant = initialParticipant, case let .member(participant) = initialParticipant, let adminInfo = participant.adminInfo, !adminInfo.rights.flags.isEmpty && admin.id != accountPeerId {
if let initialParticipant = initialParticipant, case let .member(_, _, adminInfoValue, _, _) = initialParticipant, let adminInfo = adminInfoValue, !adminInfo.rights.flags.isEmpty && admin.id != accountPeerId {
if channel.flags.contains(.isCreator) {
canDismiss = true
} else {
@ -772,6 +772,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
.canBanUsers,
.canInviteUsers,
.canPinMessages,
.canManageCalls,
.canBeAnonymous,
.canAddAdmins
]
@ -782,9 +783,9 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
if let updatedFlags = state.updatedFlags {
currentRightsFlags = updatedFlags
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights {
currentRightsFlags = adminRights.rights.flags.subtracting(.canAddAdmins)
currentRightsFlags = adminRights.rights.flags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
} else {
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins)
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
}
var index = 0
@ -1126,7 +1127,7 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
}))
}
}
} else if let _ = channelView.peers[channelView.peerId] as? TelegramGroup {
} else if let group = channelView.peers[channelView.peerId] as? TelegramGroup {
var updateFlags: TelegramChatAdminRightsFlags?
var updateRank: String?
updateState { current in
@ -1143,7 +1144,12 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
}
let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific
let defaultFlags = maskRightsFlags.subtracting(.canAddAdmins)
let defaultFlags: TelegramChatAdminRightsFlags
if case .creator = group.role {
defaultFlags = maskRightsFlags.subtracting(.canBeAnonymous)
} else {
defaultFlags = maskRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
}
if updateFlags == nil {
updateFlags = defaultFlags

View file

@ -13,6 +13,7 @@ import AccountContext
import AlertUI
import PresentationDataUtils
import ItemListPeerItem
import InviteLinksUI
private final class ChannelMembersControllerArguments {
let context: AccountContext
@ -461,7 +462,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
pushControllerImpl?(controller)
}
}, inviteViaLink: {
presentControllerImpl?(channelVisibilityController(context: context, peerId: peerId, mode: .privateLink, upgradedToSupergroup: { _, f in f() }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
pushControllerImpl?(InviteLinkInviteController(context: context, peerId: peerId))
})
let peerView = context.account.viewTracker.peerView(peerId)

View file

@ -361,16 +361,16 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
let renderedParticipant: RenderedChannelParticipant
switch participant {
case .creator:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences)
case .admin:
var peers: [PeerId: Peer] = [:]
peers[creator.id] = creator
peers[peer.id] = peer
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers, presences: peerView.peerPresences)
case .member:
var peers: [PeerId: Peer] = [:]
peers[peer.id] = peer
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers, presences: peerView.peerPresences)
}
entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled))

View file

@ -1,6 +1,7 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
@ -16,22 +17,24 @@ import AlertUI
import PresentationDataUtils
import TelegramNotices
import ItemListPeerItem
import ItemListPeerActionItem
import AccountContext
import InviteLinksUI
import ContextUI
private final class ChannelVisibilityControllerArguments {
let context: AccountContext
let updateCurrentType: (CurrentChannelType) -> Void
let updatePublicLinkText: (String?, String) -> Void
let scrollToPublicLinkText: () -> Void
let displayPrivateLinkMenu: (String) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let revokePeerId: (PeerId) -> Void
let copyPrivateLink: () -> Void
let revokePrivateLink: () -> Void
let sharePrivateLink: () -> Void
let shareLink: () -> Void
let linkContextAction: (ASDisplayNode) -> Void
let manageInviteLinks: () -> Void
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, copyPrivateLink: @escaping () -> Void, revokePrivateLink: @escaping () -> Void, sharePrivateLink: @escaping () -> Void) {
init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void, shareLink: @escaping () -> Void, linkContextAction: @escaping (ASDisplayNode) -> Void, manageInviteLinks: @escaping () -> Void) {
self.context = context
self.updateCurrentType = updateCurrentType
self.updatePublicLinkText = updatePublicLinkText
@ -39,9 +42,9 @@ private final class ChannelVisibilityControllerArguments {
self.displayPrivateLinkMenu = displayPrivateLinkMenu
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.revokePeerId = revokePeerId
self.copyPrivateLink = copyPrivateLink
self.revokePrivateLink = revokePrivateLink
self.sharePrivateLink = sharePrivateLink
self.shareLink = shareLink
self.linkContextAction = linkContextAction
self.manageInviteLinks = manageInviteLinks
}
}
@ -49,7 +52,6 @@ private enum ChannelVisibilitySection: Int32 {
case type
case link
case linkActions
case location
}
private enum ChannelVisibilityEntryTag: ItemListItemTag {
@ -73,12 +75,13 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case publicLinkHeader(PresentationTheme, String)
case publicLinkAvailability(PresentationTheme, String, Bool)
case privateLink(PresentationTheme, String, String?)
case editablePublicLink(PresentationTheme, PresentationStrings, String, String)
case privateLinkHeader(PresentationTheme, String)
case privateLink(PresentationTheme, ExportedInvitation?)
case privateLinkInfo(PresentationTheme, String)
case privateLinkCopy(PresentationTheme, String)
case privateLinkRevoke(PresentationTheme, String)
case privateLinkShare(PresentationTheme, String)
case privateLinkManage(PresentationTheme, String)
case privateLinkManageInfo(PresentationTheme, String)
case publicLinkInfo(PresentationTheme, String)
case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus)
@ -89,9 +92,9 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
switch self {
case .typeHeader, .typePublic, .typePrivate, .typeInfo:
return ChannelVisibilitySection.type.rawValue
case .publicLinkHeader, .publicLinkAvailability, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
case .publicLinkHeader, .publicLinkAvailability, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
return ChannelVisibilitySection.link.rawValue
case .privateLinkCopy, .privateLinkRevoke, .privateLinkShare:
case .privateLinkManage, .privateLinkManageInfo:
return ChannelVisibilitySection.linkActions.rawValue
case .existingLinksInfo, .existingLinkPeerItem:
return ChannelVisibilitySection.link.rawValue
@ -112,17 +115,17 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
return 4
case .publicLinkAvailability:
return 5
case .privateLink:
case .privateLinkHeader:
return 6
case .editablePublicLink:
case .privateLink:
return 7
case .privateLinkInfo:
case .editablePublicLink:
return 8
case .privateLinkCopy:
case .privateLinkInfo:
return 9
case .privateLinkRevoke:
case .privateLinkManage:
return 10
case .privateLinkShare:
case .privateLinkManageInfo:
return 11
case .publicLinkStatus:
return 12
@ -173,8 +176,14 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else {
return false
}
case let .privateLink(lhsTheme, lhsText, lhsLink):
if case let .privateLink(rhsTheme, rhsText, rhsLink) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLink == rhsLink {
case let .privateLinkHeader(lhsTheme, lhsTitle):
if case let .privateLinkHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .privateLink(lhsTheme, lhsInvite):
if case let .privateLink(rhsTheme, rhsInvite) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite {
return true
} else {
return false
@ -191,20 +200,14 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else {
return false
}
case let .privateLinkCopy(lhsTheme, lhsText):
if case let .privateLinkCopy(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .privateLinkManage(lhsTheme, lhsText):
if case let .privateLinkManage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .privateLinkRevoke(lhsTheme, lhsText):
if case let .privateLinkRevoke(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .privateLinkShare(lhsTheme, lhsText):
if case let .privateLinkShare(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
case let .privateLinkManageInfo(lhsTheme, lhsText):
if case let .privateLinkManageInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
@ -267,31 +270,35 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelVisibilityControllerArguments
switch self {
case let .typeHeader(theme, title):
case let .typeHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .typePublic(theme, text, selected):
case let .typePublic(_, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateCurrentType(.publicChannel)
})
case let .typePrivate(theme, text, selected):
case let .typePrivate(_, text, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateCurrentType(.privateChannel)
})
case let .typeInfo(theme, text):
case let .typeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .publicLinkHeader(theme, title):
case let .publicLinkHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .publicLinkAvailability(theme, text, value):
let attr = NSMutableAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor)
attr.addAttribute(.font, value: Font.regular(13), range: NSMakeRange(0, attr.length))
return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section)
case let .privateLink(theme, text, value):
return ItemListActionItem(presentationData: presentationData, title: text, kind: value != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
if let value = value {
arguments.displayPrivateLinkMenu(value)
}
}, tag: ChannelVisibilityEntryTag.privateLink)
case let .editablePublicLink(theme, strings, placeholder, currentText):
case let .privateLinkHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .privateLink(_, invite):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], buttonColor: nil, sectionId: self.section, style: .blocks, shareAction: {
arguments.shareLink()
}, contextAction: { node in
arguments.linkContextAction(node)
}, viewAction: {
})
case let .editablePublicLink(theme, _, placeholder, currentText):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), clearType: .always, tag: ChannelVisibilityEntryTag.publicLink, sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(currentText, updatedText)
}, updatedFocus: { focus in
@ -300,21 +307,15 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
}
}, action: {
})
case let .privateLinkInfo(theme, text):
case let .privateLinkInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .privateLinkCopy(theme, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyPrivateLink()
case let .privateLinkManage(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.manageInviteLinks()
})
case let .privateLinkRevoke(theme, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.revokePrivateLink()
})
case let .privateLinkShare(theme, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.sharePrivateLink()
})
case let .publicLinkInfo(theme, text):
case let .privateLinkManageInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .publicLinkInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .publicLinkStatus(theme, text, status):
var displayActivity = false
@ -336,9 +337,9 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
displayActivity = true
}
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: NSAttributedString(string: text, textColor: color), sectionId: self.section)
case let .existingLinksInfo(theme, text):
case let .existingLinksInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .existingLinkPeerItem(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
case let .existingLinkPeerItem(_, _, _, dateTimeFormat, nameDisplayOrder, peer, editing, enabled):
var label = ""
if let addressName = peer.addressName {
label = "t.me/" + addressName
@ -591,14 +592,9 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
}
}
case .privateChannel:
let link = (view.cachedData as? CachedChannelData)?.exportedInvitation?.link
let text: String
if let link = link {
text = link
} else {
text = presentationData.strings.Channel_NotificationLoading
}
entries.append(.privateLink(presentationData.theme, text, link))
let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
if isGroup {
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
} else {
@ -608,34 +604,25 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
case .initialSetup:
break
case .generic, .privateLink:
entries.append(.privateLinkCopy(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_CopyLink))
entries.append(.privateLinkRevoke(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_RevokeLink))
entries.append(.privateLinkShare(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_ShareLink))
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
}
}
} else if let _ = view.peers[view.peerId] as? TelegramGroup {
switch mode {
case .privateLink:
let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link
let text: String
if let link = link {
text = link
} else {
text = presentationData.strings.Channel_NotificationLoading
}
entries.append(.privateLink(presentationData.theme, text, link))
case .privateLink:
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_Help))
switch mode {
case .initialSetup:
break
case .generic, .privateLink:
entries.append(.privateLinkCopy(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_CopyLink))
entries.append(.privateLinkRevoke(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_RevokeLink))
entries.append(.privateLinkShare(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_ShareLink))
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
}
case .generic, .initialSetup:
let isGroup = true
case .generic, .initialSetup:
let selectedType: CurrentChannelType
if let current = state.selectedType {
selectedType = current
@ -723,22 +710,16 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp))
}
case .privateChannel:
let link = (view.cachedData as? CachedGroupData)?.exportedInvitation?.link
let text: String
if let link = link {
text = link
} else {
text = presentationData.strings.Channel_NotificationLoading
}
entries.append(.privateLink(presentationData.theme, text, link))
let invite = (view.cachedData as? CachedGroupData)?.exportedInvitation
entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
entries.append(.privateLink(presentationData.theme, invite))
entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp))
switch mode {
case .initialSetup:
break
case .generic, .privateLink:
entries.append(.privateLinkCopy(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_CopyLink))
entries.append(.privateLinkRevoke(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_RevokeLink))
entries.append(.privateLinkShare(presentationData.theme, presentationData.strings.GroupInfo_InviteLink_ShareLink))
entries.append(.privateLinkManage(presentationData.theme, presentationData.strings.InviteLink_Manage))
entries.append(.privateLinkManageInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo))
}
}
}
@ -747,7 +728,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
return entries
}
private func effectiveChannelType(state: ChannelVisibilityControllerState, peer: TelegramChannel, cachedData: CachedPeerData?) -> CurrentChannelType {
private func effectiveChannelType(mode: ChannelVisibilityControllerMode, state: ChannelVisibilityControllerState, peer: TelegramChannel, cachedData: CachedPeerData?) -> CurrentChannelType {
let selectedType: CurrentChannelType
if let current = state.selectedType {
selectedType = current
@ -756,6 +737,8 @@ private func effectiveChannelType(state: ChannelVisibilityControllerState, peer:
selectedType = .publicChannel
} else if let cachedChannelData = cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
selectedType = .publicChannel
} else if case .initialSetup = mode {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
@ -763,9 +746,9 @@ private func effectiveChannelType(state: ChannelVisibilityControllerState, peer:
return selectedType
}
private func updatedAddressName(state: ChannelVisibilityControllerState, peer: Peer, cachedData: CachedPeerData?) -> String? {
private func updatedAddressName(mode: ChannelVisibilityControllerMode, state: ChannelVisibilityControllerState, peer: Peer, cachedData: CachedPeerData?) -> String? {
if let peer = peer as? TelegramChannel {
let selectedType = effectiveChannelType(state: state, peer: peer, cachedData: cachedData)
let selectedType = effectiveChannelType(mode: mode, state: state, peer: peer, cachedData: cachedData)
let currentAddressName: String
@ -831,13 +814,13 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}))
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var nextImpl: (() -> Void)?
var displayPrivateLinkMenuImpl: ((String) -> Void)?
var scrollToPublicLinkTextImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var clearHighlightImpl: (() -> Void)?
var presentInGlobalOverlayImpl: ((ViewController) -> Void)?
var getControllerImpl: (() -> ViewController?)?
let actionsDisposable = DisposableSet()
@ -915,57 +898,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
}
})
}))
}, copyPrivateLink: {
let _ = (context.account.postbox.transaction { transaction -> String? in
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
}
return nil
} |> deliverOnMainQueue).start(next: { link in
if let link = link {
UIPasteboard.general.string = link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
}
})
}, revokePrivateLink: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in
if !state.revokingPrivateLink {
revoke = true
return state.withUpdatedRevokingPrivateLink(true)
} else {
return state
}
}
if revoke {
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
updateState {
$0.withUpdatedRevokingPrivateLink(false)
}
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, sharePrivateLink: {
}, shareLink: {
let _ = (context.account.postbox.transaction { transaction -> String? in
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
if let cachedData = cachedData as? CachedChannelData {
@ -981,6 +914,101 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
presentControllerImpl?(shareController, nil)
}
})
}, linkContextAction: { node in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.transaction { transaction -> String? in
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation?.link
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation?.link
}
}
return nil
} |> deliverOnMainQueue).start(next: { link in
if let link = link {
UIPasteboard.general.string = link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil)
}
})
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let _ = (context.account.postbox.transaction { transaction -> ExportedInvitation? in
if let cachedData = transaction.getPeerCachedData(peerId: peerId) {
if let cachedData = cachedData as? CachedChannelData {
return cachedData.exportedInvitation
} else if let cachedData = cachedData as? CachedGroupData {
return cachedData.exportedInvitation
}
}
return nil
} |> deliverOnMainQueue).start(next: { invite in
if let invite = invite {
let controller = InviteLinkQRCodeController(context: context, invite: invite)
presentControllerImpl?(controller, nil)
}
})
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in
if !state.revokingPrivateLink {
revoke = true
return state.withUpdatedRevokingPrivateLink(true)
} else {
return state
}
}
if revoke {
revokeLinkDisposable.set((ensuredExistingPeerExportedInvitation(account: context.account, peerId: peerId, revokeExisted: true) |> deliverOnMainQueue).start(completed: {
updateState {
$0.withUpdatedRevokingPrivateLink(false)
}
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})))
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: nil)
presentInGlobalOverlayImpl?(contextController)
}, manageInviteLinks: {
let controller = inviteLinkListController(context: context, peerId: peerId)
pushControllerImpl?(controller)
})
let peerView = context.account.viewTracker.peerView(peerId)
@ -1022,7 +1050,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
rightNavigationButton = ItemListNavigationButton(content: .text(mode == .initialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: {
var updatedAddressNameValue: String?
updateState { state in
updatedAddressNameValue = updatedAddressName(state: state, peer: peer, cachedData: view.cachedData)
updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: peer, cachedData: view.cachedData)
return state
}
@ -1093,7 +1121,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: {
var updatedAddressNameValue: String?
updateState { state in
updatedAddressNameValue = updatedAddressName(state: state, peer: peer, cachedData: nil)
updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: peer, cachedData: nil)
return state
}
@ -1251,9 +1279,6 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
controller.dismiss()
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
nextImpl = { [weak controller] in
if let controller = controller {
if case .initialSetup = mode {
@ -1360,8 +1385,37 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId,
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
clearHighlightImpl = { [weak controller] in
controller?.clearItemNodesHighlight(animated: true)
presentInGlobalOverlayImpl = { [weak controller] c in
if let controller = controller {
controller.presentInGlobalOverlay(c)
}
}
getControllerImpl = { [weak controller] in
return controller
}
return controller
}
private final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = false
self.blurBackground = false
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View file

@ -449,7 +449,6 @@ private class PeersNearbyControllerImpl: ItemListController {
}
}
public func peersNearbyController(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController) -> Void)?
var replaceTopControllerImpl: ((ViewController) -> Void)?

View file

@ -1341,6 +1341,7 @@ struct ChatListViewState {
case let .IntermediateMessageEntry(index, messageIndex):
var peers = SimpleDictionary<PeerId, Peer>()
var notificationsPeerId = index.messageIndex.id.peerId
var presence: PeerPresence?
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
peers[peer.id] = peer
if let notificationSettingsPeerId = peer.notificationSettingsPeerId {
@ -1350,6 +1351,9 @@ struct ChatListViewState {
if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
peers[associatedPeer.id] = associatedPeer
}
presence = postbox.peerPresenceTable.get(associatedPeerId)
} else {
presence = postbox.peerPresenceTable.get(index.messageIndex.id.peerId)
}
}
let renderedPeer = RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers)
@ -1387,7 +1391,7 @@ struct ChatListViewState {
}
}
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: renderedPeer, presence: postbox.peerPresenceTable.get(index.messageIndex.id.peerId), tagSummaryInfo: tagSummaryInfo, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId))
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, messages: renderedMessages, readState: postbox.readStateTable.getCombinedState(index.messageIndex.id.peerId), notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState, renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId))
if directionIndex == 0 {
self.stateBySpace[space]!.orderedEntries.setLowerOrAtAnchorAtArrayIndex(listIndex, to: updatedEntry)
} else {

View file

@ -2787,7 +2787,11 @@ public final class Postbox {
let mutableView = MutableChatListView(postbox: self, groupId: groupId, filterPredicate: filterPredicate, aroundIndex: index, count: count, summaryComponents: summaryComponents)
mutableView.render(postbox: self, renderMessage: self.renderIntermediateMessage, getPeer: { id in
return self.peerTable.get(id)
}, getPeerNotificationSettings: { self.peerNotificationSettingsTable.getEffective($0) }, getPeerPresence: { self.peerPresenceTable.get($0) })
}, getPeerNotificationSettings: {
self.peerNotificationSettingsTable.getEffective($0)
}, getPeerPresence: {
self.peerPresenceTable.get($0)
})
let (index, signal) = self.viewTracker.addChatListView(mutableView)

View file

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SectionHeaderItem",
module_name = "SectionHeaderItem",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ItemListUI:ItemListUI",
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,119 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import ItemListUI
public class SectionHeaderItem: ListViewItem {
let presentationData: ItemListPresentationData
let title: String
public init(presentationData: ItemListPresentationData, title: String) {
self.presentationData = presentationData
self.title = title
}
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 = SectionHeaderItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
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? SectionHeaderItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
private class SectionHeaderItemNode: ListViewItemNode {
private var headerNode: ListSectionHeaderNode?
private var item: SectionHeaderItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: SectionHeaderItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
return { [weak self] item, params in
let contentSize = CGSize(width: params.width, height: 28.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let headerNode: ListSectionHeaderNode
if let currentHeaderNode = strongSelf.headerNode {
headerNode = currentHeaderNode
if let _ = updatedTheme {
headerNode.updateTheme(theme: item.presentationData.theme)
}
} else {
headerNode = ListSectionHeaderNode(theme: item.presentationData.theme)
headerNode.title = item.title
strongSelf.addSubnode(headerNode)
strongSelf.headerNode = headerNode
}
headerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
headerNode.updateLayout(size: contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
}

View file

@ -530,7 +530,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
return .single(false)
}
} else {
return .complete()
return .single(hasTwoStepAuth)
}
}

View file

@ -78,7 +78,7 @@ final class SettingsSearchItem: ItemListControllerSearch {
let exceptionsList: Signal<NotificationExceptionsList?, NoError>
let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>
let privacySettings: Signal<AccountPrivacySettings?, NoError>
let hasWallet: Signal<Bool, NoError>
let hasTwoStepAuth: Signal<Bool?, NoError>
let activeSessionsContext: Signal<ActiveSessionsContext?, NoError>
let webSessionsContext: Signal<WebSessionsContext?, NoError>
@ -86,7 +86,7 @@ final class SettingsSearchItem: ItemListControllerSearch {
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
init(context: AccountContext, theme: PresentationTheme, placeholder: String, activated: Bool, updateActivated: @escaping (Bool) -> Void, presentController: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.context = context
self.theme = theme
self.placeholder = placeholder
@ -99,7 +99,7 @@ final class SettingsSearchItem: ItemListControllerSearch {
self.exceptionsList = exceptionsList
self.archivedStickerPacks = archivedStickerPacks
self.privacySettings = privacySettings
self.hasWallet = hasWallet
self.hasTwoStepAuth = hasTwoStepAuth
self.activeSessionsContext = activeSessionsContext
self.webSessionsContext = webSessionsContext
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
@ -165,7 +165,7 @@ final class SettingsSearchItem: ItemListControllerSearch {
pushController(c)
}, presentController: { c, a in
presentController(c, a)
}, getNavigationController: self.getNavigationController, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext)
}, getNavigationController: self.getNavigationController, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext)
}
}
}
@ -362,7 +362,7 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationDataPromise = Promise(self.presentationData)
@ -389,7 +389,7 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo
})
let searchableItems = Promise<[SettingsSearchableItem]>()
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasWallet: hasWallet, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasTwoStepAuth: hasTwoStepAuth, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext))
let faqItems = Promise<[SettingsSearchableItem]>()
faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false))
@ -662,13 +662,13 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
let exceptionsList: Signal<NotificationExceptionsList?, NoError>
let archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>
let privacySettings: Signal<AccountPrivacySettings?, NoError>
let hasWallet: Signal<Bool, NoError>
let hasTwoStepAuth: Signal<Bool?, NoError>
let activeSessionsContext: Signal<ActiveSessionsContext?, NoError>
let webSessionsContext: Signal<WebSessionsContext?, NoError>
var cancel: () -> Void
init(context: AccountContext, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
init(context: AccountContext, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: (() -> NavigationController?)?, resolvedFaqUrl: Signal<ResolvedUrl?, NoError>, exceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.cancel = cancel
@ -679,7 +679,7 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
self.exceptionsList = exceptionsList
self.archivedStickerPacks = archivedStickerPacks
self.privacySettings = privacySettings
self.hasWallet = hasWallet
self.hasTwoStepAuth = hasTwoStepAuth
self.activeSessionsContext = activeSessionsContext
self.webSessionsContext = webSessionsContext
@ -721,7 +721,7 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode {
}
})
}
}, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasWallet: self.hasWallet, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in
}, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in
self?.cancel()
})

View file

@ -729,7 +729,7 @@ private func languageSearchableItems(context: AccountContext, localizations: [Lo
return items
}
func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasWallet: Signal<Bool, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) -> Signal<[SettingsSearchableItem], NoError> {
func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal<NotificationExceptionsList?, NoError>, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal<AccountPrivacySettings?, NoError>, hasTwoStepAuth: Signal<Bool?, NoError>, activeSessionsContext: Signal<ActiveSessionsContext?, NoError>, webSessionsContext: Signal<WebSessionsContext?, NoError>) -> Signal<[SettingsSearchableItem], NoError> {
let watchAppInstalled = (context.watchManager?.watchAppInstalled ?? .single(false))
|> take(1)
@ -825,8 +825,8 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
}
}
return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet, activeSessionsContext, activeWebSessionsContext)
|> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasWallet, activeSessionsContext, activeWebSessionsContext in
return combineLatest(watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasTwoStepAuth, activeSessionsContext, activeWebSessionsContext)
|> map { watchAppInstalled, canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasTwoStepAuth, activeSessionsContext, activeWebSessionsContext in
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
var allItems: [SettingsSearchableItem] = []
@ -875,21 +875,12 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
allItems.append(watch)
}
let passport = SettingsSearchableItem(id: .passport(0), title: strings.Settings_Passport, alternate: synonyms(strings.SettingsSearch_Synonyms_Passport), icon: .passport, breadcrumbs: [], present: { context, _, present in
present(.modal, SecureIdAuthController(context: context, mode: .list))
})
allItems.append(passport)
#if ENABLE_WALLET
if hasWallet {
let wallet = SettingsSearchableItem(id: .wallet(0), title: strings.Settings_Wallet, alternate: synonyms(strings.SettingsSearch_Synonyms_Wallet), icon: .wallet, breadcrumbs: [], present: { context, _, present in
context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in
present(.push, c)
})
if let hasTwoStepAuth = hasTwoStepAuth, hasTwoStepAuth {
let passport = SettingsSearchableItem(id: .passport(0), title: strings.Settings_Passport, alternate: synonyms(strings.SettingsSearch_Synonyms_Passport), icon: .passport, breadcrumbs: [], present: { context, _, present in
present(.modal, SecureIdAuthController(context: context, mode: .list))
})
allItems.append(wallet)
allItems.append(passport)
}
#endif
let support = SettingsSearchableItem(id: .support(0), title: strings.Settings_Support, alternate: synonyms(strings.SettingsSearch_Synonyms_Support), icon: .support, breadcrumbs: [], present: { context, _, present in
let _ = (supportPeerId(account: context.account)

View file

@ -135,6 +135,8 @@ private func collectExternalShareItems(strings: PresentationStrings, dateTimeFor
fileName = value
} else if file.isVideo {
fileName = "telegram_video.mp4"
} else if file.isVoice {
fileName = "telegram_audio.ogg"
} else {
fileName = "file"
}

View file

@ -406,9 +406,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol
let minimallyRevealedRowCount: CGFloat = 3.5
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - buttonHeight)
let bottomGridInset = hasShareButton ? buttonHeight * 2.0 : buttonHeight
let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - bottomGridInset)
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
if let activityIndicator = self.activityIndicator {

View file

@ -6,16 +6,18 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
public let isRevoked: Bool
public let adminId: PeerId
public let date: Int32
public let startDate: Int32?
public let expireDate: Int32?
public let usageLimit: Int32?
public let count: Int32?
public init(link: String, isPermanent: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, expireDate: Int32?, usageLimit: Int32?, count: Int32?) {
public init(link: String, isPermanent: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, count: Int32?) {
self.link = link
self.isPermanent = isPermanent
self.isRevoked = isRevoked
self.adminId = adminId
self.date = date
self.startDate = startDate
self.expireDate = expireDate
self.usageLimit = usageLimit
self.count = count
@ -27,6 +29,7 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
self.isRevoked = decoder.decodeBoolForKey("revoked", orElse: false)
self.adminId = PeerId(decoder.decodeInt64ForKey("adminId", orElse: 0))
self.date = decoder.decodeInt32ForKey("date", orElse: 0)
self.startDate = decoder.decodeOptionalInt32ForKey("startDate")
self.expireDate = decoder.decodeOptionalInt32ForKey("expireDate")
self.usageLimit = decoder.decodeOptionalInt32ForKey("usageLimit")
self.count = decoder.decodeOptionalInt32ForKey("count")
@ -38,6 +41,11 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
encoder.encodeBool(self.isRevoked, forKey: "revoked")
encoder.encodeInt64(self.adminId.toInt64(), forKey: "adminId")
encoder.encodeInt32(self.date, forKey: "date")
if let startDate = self.startDate {
encoder.encodeInt32(startDate, forKey: "startDate")
} else {
encoder.encodeNil(forKey: "startDate")
}
if let expireDate = self.expireDate {
encoder.encodeInt32(expireDate, forKey: "expireDate")
} else {
@ -56,6 +64,6 @@ public struct ExportedInvitation: PostboxCoding, Equatable {
}
public static func ==(lhs: ExportedInvitation, rhs: ExportedInvitation) -> Bool {
return lhs.link == rhs.link && lhs.isPermanent == rhs.isPermanent && lhs.isRevoked == rhs.isRevoked && lhs.adminId == rhs.adminId && lhs.date == rhs.date && lhs.expireDate == rhs.expireDate && lhs.usageLimit == rhs.usageLimit && lhs.count == rhs.count
return lhs.link == rhs.link && lhs.isPermanent == rhs.isPermanent && lhs.isRevoked == rhs.isRevoked && lhs.adminId == rhs.adminId && lhs.date == rhs.date && lhs.startDate == rhs.startDate && lhs.expireDate == rhs.expireDate && lhs.usageLimit == rhs.usageLimit && lhs.count == rhs.count
}
}

View file

@ -22,7 +22,7 @@ public struct TelegramChatAdminRightsFlags: OptionSet {
public static let canBeAnonymous = TelegramChatAdminRightsFlags(rawValue: 1 << 10)
public static let canManageCalls = TelegramChatAdminRightsFlags(rawValue: 1 << 11)
public static var all:TelegramChatAdminRightsFlags {
public static var all: TelegramChatAdminRightsFlags {
return [.canChangeInfo, .canPostMessages, .canEditMessages, .canDeleteMessages, .canBanUsers, .canInviteUsers, .canPinMessages, .canAddAdmins, .canBeAnonymous, .canManageCalls]
}
@ -34,8 +34,7 @@ public struct TelegramChatAdminRightsFlags: OptionSet {
.canPinMessages,
.canManageCalls,
.canBeAnonymous,
.canAddAdmins,
.canManageCalls
.canAddAdmins
]
public static var broadcastSpecific: TelegramChatAdminRightsFlags = [

View file

@ -737,7 +737,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1634752813] = { return Api.messages.FavedStickers.parse_favedStickersNotModified($0) }
dict[-209768682] = { return Api.messages.FavedStickers.parse_favedStickers($0) }
dict[1776236393] = { return Api.ExportedChatInvite.parse_chatInviteEmpty($0) }
dict[-1448589334] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) }
dict[1847917725] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) }
dict[-1389486888] = { return Api.account.AuthorizationForm.parse_authorizationForm($0) }
dict[-1392388579] = { return Api.Authorization.parse_authorization($0) }
dict[-1361650766] = { return Api.MaskCoords.parse_maskCoords($0) }
@ -869,8 +869,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1417756512] = { return Api.EncryptedChat.parse_encryptedChatEmpty($0) }
dict[1006044124] = { return Api.EncryptedChat.parse_encryptedChatWaiting($0) }
dict[-94974410] = { return Api.EncryptedChat.parse_encryptedChat($0) }
dict[332848423] = { return Api.EncryptedChat.parse_encryptedChatDiscarded($0) }
dict[1651608194] = { return Api.EncryptedChat.parse_encryptedChatRequested($0) }
dict[505183301] = { return Api.EncryptedChat.parse_encryptedChatDiscarded($0) }
dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) }
dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) }
dict[922273905] = { return Api.Document.parse_documentEmpty($0) }

View file

@ -20979,7 +20979,7 @@ public extension Api {
}
public enum ExportedChatInvite: TypeConstructorDescription {
case chatInviteEmpty
case chatInviteExported(flags: Int32, link: String, adminId: Int32, date: Int32, expireDate: Int32?, usageLimit: Int32?, usage: Int32?)
case chatInviteExported(flags: Int32, link: String, adminId: Int32, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -20989,14 +20989,15 @@ public extension Api {
}
break
case .chatInviteExported(let flags, let link, let adminId, let date, let expireDate, let usageLimit, let usage):
case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage):
if boxed {
buffer.appendInt32(-1448589334)
buffer.appendInt32(1847917725)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(link, buffer: buffer, boxed: false)
serializeInt32(adminId, buffer: buffer, boxed: false)
serializeInt32(date, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 4) != 0 {serializeInt32(startDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(expireDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(usageLimit!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeInt32(usage!, buffer: buffer, boxed: false)}
@ -21008,8 +21009,8 @@ public extension Api {
switch self {
case .chatInviteEmpty:
return ("chatInviteEmpty", [])
case .chatInviteExported(let flags, let link, let adminId, let date, let expireDate, let usageLimit, let usage):
return ("chatInviteExported", [("flags", flags), ("link", link), ("adminId", adminId), ("date", date), ("expireDate", expireDate), ("usageLimit", usageLimit), ("usage", usage)])
case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage):
return ("chatInviteExported", [("flags", flags), ("link", link), ("adminId", adminId), ("date", date), ("startDate", startDate), ("expireDate", expireDate), ("usageLimit", usageLimit), ("usage", usage)])
}
}
@ -21026,20 +21027,23 @@ public extension Api {
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt32() }
var _6: Int32?
if Int(_1!) & Int(1 << 2) != 0 {_6 = reader.readInt32() }
if Int(_1!) & Int(1 << 1) != 0 {_6 = reader.readInt32() }
var _7: Int32?
if Int(_1!) & Int(1 << 3) != 0 {_7 = reader.readInt32() }
if Int(_1!) & Int(1 << 2) != 0 {_7 = reader.readInt32() }
var _8: Int32?
if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, expireDate: _5, usageLimit: _6, usage: _7)
let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8)
}
else {
return nil
@ -24065,8 +24069,8 @@ public extension Api {
case encryptedChatEmpty(id: Int32)
case encryptedChatWaiting(id: Int32, accessHash: Int64, date: Int32, adminId: Int32, participantId: Int32)
case encryptedChat(id: Int32, accessHash: Int64, date: Int32, adminId: Int32, participantId: Int32, gAOrB: Buffer, keyFingerprint: Int64)
case encryptedChatDiscarded(id: Int32)
case encryptedChatRequested(flags: Int32, folderId: Int32?, id: Int32, accessHash: Int64, date: Int32, adminId: Int32, participantId: Int32, gA: Buffer)
case encryptedChatDiscarded(flags: Int32, id: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -24098,12 +24102,6 @@ public extension Api {
serializeBytes(gAOrB, buffer: buffer, boxed: false)
serializeInt64(keyFingerprint, buffer: buffer, boxed: false)
break
case .encryptedChatDiscarded(let id):
if boxed {
buffer.appendInt32(332848423)
}
serializeInt32(id, buffer: buffer, boxed: false)
break
case .encryptedChatRequested(let flags, let folderId, let id, let accessHash, let date, let adminId, let participantId, let gA):
if boxed {
buffer.appendInt32(1651608194)
@ -24117,6 +24115,13 @@ public extension Api {
serializeInt32(participantId, buffer: buffer, boxed: false)
serializeBytes(gA, buffer: buffer, boxed: false)
break
case .encryptedChatDiscarded(let flags, let id):
if boxed {
buffer.appendInt32(505183301)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
break
}
}
@ -24128,10 +24133,10 @@ public extension Api {
return ("encryptedChatWaiting", [("id", id), ("accessHash", accessHash), ("date", date), ("adminId", adminId), ("participantId", participantId)])
case .encryptedChat(let id, let accessHash, let date, let adminId, let participantId, let gAOrB, let keyFingerprint):
return ("encryptedChat", [("id", id), ("accessHash", accessHash), ("date", date), ("adminId", adminId), ("participantId", participantId), ("gAOrB", gAOrB), ("keyFingerprint", keyFingerprint)])
case .encryptedChatDiscarded(let id):
return ("encryptedChatDiscarded", [("id", id)])
case .encryptedChatRequested(let flags, let folderId, let id, let accessHash, let date, let adminId, let participantId, let gA):
return ("encryptedChatRequested", [("flags", flags), ("folderId", folderId), ("id", id), ("accessHash", accessHash), ("date", date), ("adminId", adminId), ("participantId", participantId), ("gA", gA)])
case .encryptedChatDiscarded(let flags, let id):
return ("encryptedChatDiscarded", [("flags", flags), ("id", id)])
}
}
@ -24198,17 +24203,6 @@ public extension Api {
return nil
}
}
public static func parse_encryptedChatDiscarded(_ reader: BufferReader) -> EncryptedChat? {
var _1: Int32?
_1 = reader.readInt32()
let _c1 = _1 != nil
if _c1 {
return Api.EncryptedChat.encryptedChatDiscarded(id: _1!)
}
else {
return nil
}
}
public static func parse_encryptedChatRequested(_ reader: BufferReader) -> EncryptedChat? {
var _1: Int32?
_1 = reader.readInt32()
@ -24241,6 +24235,20 @@ public extension Api {
return nil
}
}
public static func parse_encryptedChatDiscarded(_ reader: BufferReader) -> EncryptedChat? {
var _1: Int32?
_1 = reader.readInt32()
var _2: Int32?
_2 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.EncryptedChat.encryptedChatDiscarded(flags: _1!, id: _2!)
}
else {
return nil
}
}
}
public enum PeerLocated: TypeConstructorDescription {

View file

@ -2109,20 +2109,6 @@ public extension Api {
})
}
public static func discardEncryption(chatId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-304536635)
serializeInt32(chatId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.discardEncryption", parameters: [("chatId", chatId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func setEncryptedTyping(peer: Api.InputEncryptedChat, typing: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(2031374829)
@ -3903,24 +3889,6 @@ public extension Api {
})
}
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
let buffer = Buffer()
buffer.appendInt32(1838984707)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
let reader = BufferReader(buffer)
var result: Api.messages.ExportedChatInvites?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.ExportedChatInvites
}
return result
})
}
public static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.ExportedChatInvite>) {
let buffer = Buffer()
buffer.appendInt32(347716823)
@ -3973,6 +3941,96 @@ public extension Api {
return result
})
}
public static func getExportedChatInvites(flags: Int32, peer: Api.InputPeer, adminId: Api.InputUser?, offsetLink: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.ExportedChatInvites>) {
let buffer = Buffer()
buffer.appendInt32(1838984707)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {adminId!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(offsetLink!, buffer: buffer, boxed: false)}
serializeInt32(limit, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.getExportedChatInvites", parameters: [("flags", flags), ("peer", peer), ("adminId", adminId), ("offsetLink", offsetLink), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ExportedChatInvites? in
let reader = BufferReader(buffer)
var result: Api.messages.ExportedChatInvites?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.ExportedChatInvites
}
return result
})
}
public static func deleteRevokedExportedChatInvites(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1375999075)
peer.serialize(buffer, true)
return (FunctionDescription(name: "messages.deleteRevokedExportedChatInvites", parameters: [("peer", peer)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func deleteExportedChatInvite(peer: Api.InputPeer, link: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-731601877)
peer.serialize(buffer, true)
serializeString(link, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.deleteExportedChatInvite", parameters: [("peer", peer), ("link", link)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func discardEncryption(flags: Int32, chatId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-208425312)
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(chatId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.discardEncryption", parameters: [("flags", flags), ("chatId", chatId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func deleteChat(chatId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-2094760687)
serializeInt32(chatId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.deleteChat", parameters: [("chatId", chatId)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func deletePhoneCallHistory(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedHistory>) {
let buffer = Buffer()
buffer.appendInt32(1828657989)
serializeInt32(flags, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.deletePhoneCallHistory", parameters: [("flags", flags)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AffectedHistory? in
let reader = BufferReader(buffer)
var result: Api.messages.AffectedHistory?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.AffectedHistory
}
return result
})
}
}
public struct channels {
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {

View file

@ -398,10 +398,16 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
if scrollView.isDecelerating {
self.changeTrack()
}
self.rateButton.alpha = 0.0
self.rateButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.changeTrack()
self.rateButton.alpha = 1.0
self.rateButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
@ -409,6 +415,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
return
}
self.changeTrack()
self.rateButton.alpha = 1.0
self.rateButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
private func changeTrack() {
@ -457,9 +466,11 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.rightMaskNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.rightMaskNode.frame = CGRect(x: size.width - inset - 12.0, y: 0.0, width: 12.0, height: minHeight)
self.scrollNode.view.contentSize = contentSize
self.scrollNode.view.contentOffset = CGPoint(x: contentOffset, y: 0.0)
self.initialContentOffset = contentOffset
if !self.scrollNode.view.isTracking && !self.scrollNode.view.isTracking {
self.scrollNode.view.contentSize = contentSize
self.scrollNode.view.contentOffset = CGPoint(x: contentOffset, y: 0.0)
self.initialContentOffset = contentOffset
}
let bounds = CGRect(origin: CGPoint(), size: size)
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))

View file

@ -483,7 +483,8 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.growingForegroundCircleLayer.isHidden = true
self.foregroundGradientLayer.type = .radial
self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor]
self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
@ -655,16 +656,16 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
let targetScale: CGFloat
if let active = active {
if active {
targetColors = [activeBlue.cgColor, green.cgColor]
targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor]
targetScale = 0.89
outerColor = UIColor(rgb: 0x21674f)
} else {
targetColors = [lightBlue.cgColor, blue.cgColor]
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.85
outerColor = UIColor(rgb: 0x1d588d)
}
} else {
targetColors = [lightBlue.cgColor, blue.cgColor]
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.3
outerColor = nil
}

View file

@ -1035,6 +1035,14 @@ public final class VoiceChatController: ViewController {
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers?.totalCount ?? 0)))
strongSelf.currentSubtitle = subtitle
if let callState = strongSelf.callState, callState.canManageCall {
strongSelf.optionsButton.isUserInteractionEnabled = true
strongSelf.optionsButton.alpha = 1.0
} else {
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
}
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
}
@ -1074,27 +1082,6 @@ public final class VoiceChatController: ViewController {
strongSelf.didSetDataReady = true
strongSelf.controller?.dataReady.set(true)
}
if let peer = peerViewMainPeer(view) {
if let channel = peer as? TelegramChannel {
if channel.hasPermission(.manageCalls) {
strongSelf.optionsButton.isUserInteractionEnabled = true
strongSelf.optionsButton.alpha = 1.0
} else {
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
strongSelf.optionsButton.isUserInteractionEnabled = true
strongSelf.optionsButton.alpha = 1.0
default:
strongSelf.optionsButton.isUserInteractionEnabled = false
strongSelf.optionsButton.alpha = 0.0
}
}
}
})
self.audioOutputStateDisposable = (call.audioOutputState
@ -1142,7 +1129,7 @@ public final class VoiceChatController: ViewController {
self.audioOutputNode.addTarget(self, action: #selector(self.audioOutputPressed), forControlEvents: .touchUpInside)
self.optionsButton.contextAction = { [weak self, weak optionsButton] sourceNode, gesture in
self.optionsButton.contextAction = { [weak self] sourceNode, gesture in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
@ -1181,16 +1168,7 @@ public final class VoiceChatController: ViewController {
strongSelf.call.updateDefaultParticipantsAreMuted(isMuted: true)
})))
}
if !items.isEmpty {
items.append(.separator)
}
items.append(.custom(VoiceChatRecordingContextItem(timestamp: CFAbsoluteTimeGetCurrent(), action: { (_, f) in
f(.dismissWithoutContent)
}), false))
if !items.isEmpty {
items.append(.separator)
}
@ -1225,6 +1203,10 @@ public final class VoiceChatController: ViewController {
})))
}
if items.isEmpty {
return
}
let optionsButton: VoiceChatHeaderButton
if !strongSelf.recButton.isHidden {
optionsButton = strongSelf.recButton

View file

@ -9,8 +9,8 @@ extension ExportedInvitation {
switch apiExportedInvite {
case .chatInviteEmpty:
return nil
case let .chatInviteExported(flags, link, adminId, date, expireDate, usageLimit, usage):
self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), date: date, expireDate: expireDate, usageLimit: usageLimit, count: usage)
case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage):
self = ExportedInvitation(link: link, isPermanent: (flags & (1 << 5)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: adminId), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage)
}
}
}

View file

@ -9,7 +9,7 @@ import SyncCore
public func ensuredExistingPeerExportedInvitation(account: Account, peerId: PeerId, revokeExisted: Bool = false) -> Signal<ExportedInvitation?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = (1 << 2)
let flags: Int32 = (1 << 2)
if let _ = peer as? TelegramChannel {
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, cachedData.exportedInvitation != nil && !revokeExisted {
return .complete()
@ -90,17 +90,26 @@ public func createPeerExportedInvitation(account: Account, peerId: PeerId, expir
} |> switchToLatest
}
public func peerExportedInvitations(account: Account, peerId: PeerId) -> Signal<[ExportedInvitation]?, NoError> {
return account.postbox.transaction { transaction -> Signal<[ExportedInvitation]?, NoError> in
public struct ExportedInvitations : Equatable {
public let list: [ExportedInvitation]?
public let totalCount: Int32
}
public func peerExportedInvitations(account: Account, peerId: PeerId, revoked: Bool, offsetLink: String? = nil) -> Signal<ExportedInvitations?, NoError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitations?, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: 0, peer: inputPeer, adminId: nil, offsetLink: nil, limit: 100))
var flags: Int32 = 0
if revoked {
flags |= (1 << 3)
}
return account.network.request(Api.functions.messages.getExportedChatInvites(flags: flags, peer: inputPeer, adminId: nil, offsetLink: nil, limit: 50))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.ExportedChatInvites?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<[ExportedInvitation]?, NoError> in
return account.postbox.transaction { transaction -> [ExportedInvitation]? in
if let result = result, case let .exportedChatInvites(_, apiInvites, users) = result {
|> mapToSignal { result -> Signal<ExportedInvitations?, NoError> in
return account.postbox.transaction { transaction -> ExportedInvitations? in
if let result = result, case let .exportedChatInvites(count, apiInvites, users) = result {
var peers: [Peer] = []
var peersMap: [PeerId: Peer] = [:]
for user in users {
@ -118,7 +127,7 @@ public func peerExportedInvitations(account: Account, peerId: PeerId) -> Signal<
invites.append(invite)
}
}
return invites
return ExportedInvitations(list: invites, totalCount: count)
} else {
return nil
}
@ -146,20 +155,22 @@ public func editPeerExportedInvitation(account: Account, peerId: PeerId, link: S
}
return account.network.request(Api.functions.messages.editExportedChatInvite(flags: flags, peer: inputPeer, link: link, expireDate: expireDate, usageLimit: usageLimit))
|> mapError { _ in return EditPeerExportedInvitationError.generic }
|> map { result -> ExportedInvitation? in
if case let .exportedChatInvite(invite, users) = result {
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
|> mapToSignal { result -> Signal<ExportedInvitation?, EditPeerExportedInvitationError> in
return account.postbox.transaction { transaction in
if case let .exportedChatInvite(invite, users) = result {
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: invite)
} else {
return nil
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: invite)
} else {
return nil
}
} |> mapError { _ in .generic }
}
} else {
return .complete()
@ -173,13 +184,29 @@ public enum RevokePeerExportedInvitationError {
case generic
}
public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal<Never, RevokePeerExportedInvitationError> {
return account.postbox.transaction { transaction -> Signal<Never, RevokePeerExportedInvitationError> in
public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal<ExportedInvitation?, RevokePeerExportedInvitationError> {
return account.postbox.transaction { transaction -> Signal<ExportedInvitation?, RevokePeerExportedInvitationError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
let flags: Int32 = (1 << 2)
return account.network.request(Api.functions.messages.editExportedChatInvite(flags: flags, peer: inputPeer, link: link, expireDate: nil, usageLimit: nil))
|> mapError { _ in return RevokePeerExportedInvitationError.generic }
|> ignoreValues
|> mapToSignal { result -> Signal<ExportedInvitation?, RevokePeerExportedInvitationError> in
return account.postbox.transaction { transaction in
if case let .exportedChatInvite(invite, users) = result {
var peers: [Peer] = []
for user in users {
let telegramUser = TelegramUser(user: user)
peers.append(telegramUser)
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
return updated
})
return ExportedInvitation(apiExportedInvite: invite)
} else {
return nil
}
} |> mapError { _ in .generic }
}
} else {
return .complete()
}
@ -188,6 +215,39 @@ public func revokePeerExportedInvitation(account: Account, peerId: PeerId, link:
|> switchToLatest
}
public enum DeletePeerExportedInvitationError {
case generic
}
public func deletePeerExportedInvitation(account: Account, peerId: PeerId, link: String) -> Signal<Never, DeletePeerExportedInvitationError> {
return account.postbox.transaction { transaction -> Signal<Never, DeletePeerExportedInvitationError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.deleteExportedChatInvite(peer: inputPeer, link: link))
|> mapError { _ in return DeletePeerExportedInvitationError.generic }
|> ignoreValues
} else {
return .complete()
}
}
|> castError(DeletePeerExportedInvitationError.self)
|> switchToLatest
}
public func deleteAllRevokedPeerExportedInvitations(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.deleteRevokedExportedChatInvites(peer: inputPeer))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
} else {
return .complete()
}
}
|> switchToLatest
}
private let cachedPeerInvitationImportersCollectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 20)
public struct PeerInvitationImportersState: Equatable {

View file

@ -1674,7 +1674,7 @@ private func sendBoxedDecryptedMessage(postbox: Postbox, network: Network, peer:
}
private func requestTerminateSecretChat(postbox: Postbox, network: Network, peerId: PeerId, tagLocalIndex: Int32, reportSpam: Bool) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.discardEncryption(chatId: peerId.id))
return network.request(Api.functions.messages.discardEncryption(flags: 0, chatId: peerId.id))
|> map(Optional.init)
|> `catch` { _ in
return .single(nil)

View file

@ -34,7 +34,7 @@ public func updatePeersNearbyVisibility(account: Account, update: PeerNearbyVisi
case let .visible(latitude, longitude):
flags |= (1 << 0)
geoPoint = .inputGeoPoint(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)
selfExpires = 0x7fffffff
selfExpires = 10800
case let .location(latitude, longitude):
geoPoint = .inputGeoPoint(flags: 0, lat: latitude, long: longitude, accuracyRadius: nil)
case .invisible:
@ -63,6 +63,15 @@ public func updatePeersNearbyVisibility(account: Account, update: PeerNearbyVisi
|> map(Optional.init)
|> `catch` { error -> Signal<Api.Updates?, NoError> in
if error.errorCode == 406 {
if error.errorDescription == "USERPIC_PRIVACY_REQUIRED" {
let _ = (account.postbox.transaction { transaction in
transaction.updatePreferencesEntry(key: PreferencesKeys.peersNearby, { entry in
var settings = entry as? PeersNearbyState ?? PeersNearbyState.default
settings.visibilityExpires = nil
return settings
})
}).start()
}
return .single(nil)
} else {
return .single(nil)

View file

@ -138,7 +138,7 @@ public extension TelegramChannel {
}
}
public func hasBannedPermission(_ rights: TelegramChatBannedRightsFlags) -> (Int32, Bool)? {
func hasBannedPermission(_ rights: TelegramChatBannedRightsFlags) -> (Int32, Bool)? {
if self.flags.contains(.isCreator) {
return nil
}
@ -154,7 +154,7 @@ public extension TelegramChannel {
return nil
}
public var isRestrictedBySlowmode: Bool {
var isRestrictedBySlowmode: Bool {
if self.flags.contains(.isCreator) {
return false
}

View file

@ -66,7 +66,7 @@ func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: Pee
} else {
Logger.shared.log("State", "got encryptedChat, but peer or state don't exist or account is not creator")
}
case .encryptedChatDiscarded(_):
case let .encryptedChatDiscarded(flags, _):
if let currentPeer = currentPeer, let currentState = currentState {
let state = currentState.withUpdatedEmbeddedState(.terminated)
let peer = currentPeer.withUpdatedEmbeddedState(state.embeddedState.peerState)

View file

@ -556,7 +556,7 @@ extension Api.EncryptedChat {
switch self {
case let .encryptedChat(id, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: id)
case let .encryptedChatDiscarded(id):
case let .encryptedChatDiscarded(_, id):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: id)
case let .encryptedChatEmpty(id):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: id)

View file

@ -38,8 +38,10 @@ public enum PresentationResourceKey: Int32 {
case itemListCheckIcon
case itemListSecondaryCheckIcon
case itemListPlusIcon
case itemListDeleteIcon
case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon
case itemListLinkIcon
case itemListAddPersonIcon
case itemListCreateGroupIcon
case itemListAddExceptionIcon

View file

@ -51,6 +51,12 @@ public struct PresentationResourcesItemList {
})
}
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)
})
}
public static func stickerUnreadDotImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListStickerItemUnreadDot.rawValue, { theme in
return generateFilledCircleImage(diameter: 6.0, color: theme.list.itemAccentColor)
@ -90,6 +96,12 @@ public struct PresentationResourcesItemList {
})
}
public static func linkIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListLinkIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Contact List/LinkActionIcon"), color: theme.list.itemAccentColor)
})
}
public static func addPersonIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListAddPersonIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Contact List/AddMemberIcon"), color: theme.list.itemAccentColor)

View file

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View file

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_editaccount.pdf",
"filename" : "ic_linkexpired.pdf",
"idiom" : "universal"
}
],

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_linkfire.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_link.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_qrlogo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View file

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "NavigationSettingsIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,22 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "QrGem@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "QrGem@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Group2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_walletupdate.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Group.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SmallGem.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Some files were not shown because too many files have changed in this diff Show more