mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various Improvements
This commit is contained in:
parent
fadd3abea6
commit
fa125d0192
124 changed files with 12748 additions and 5704 deletions
BIN
Telegram/Telegram-iOS/Resources/Invite.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Invite.tgs
Normal file
Binary file not shown.
BIN
Telegram/Telegram-iOS/Resources/PlaneLogo.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/PlaneLogo.tgs
Normal file
Binary file not shown.
17
Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist
Normal file
17
Telegram/Telegram-iOS/be.lproj/AppIntentVocabulary.plist
Normal 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'll be there in 10 minutes</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
12
Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
Normal file
12
Telegram/Telegram-iOS/be.lproj/InfoPlist.strings
Normal 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 для разблакіравання праграмы.";
|
||||
0
Telegram/Telegram-iOS/be.lproj/Localizable.strings
Normal file
0
Telegram/Telegram-iOS/be.lproj/Localizable.strings
Normal 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";
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. ";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями.";
|
||||
"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео.";
|
||||
"NSPhotoLibraryUsageDescription" = "Это необходимо, чтобы вы могли делиться фото и видео из библиотеки устройства.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Это необходимо, чтобы вы могли сохранять фото и видео в библиотеку устройства.";
|
||||
"NSMicrophoneUsageDescription" = "Это необходимо, чтобы вы могли делиться голосовыми сообщениями и видео со звуком.";
|
||||
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений";
|
||||
"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений.";
|
||||
"NSFaceIDUsageDescription" = "Вы можете разблокировать приложение с помощью Face ID.";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)?
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
17
submodules/DatePickerNode/BUILD
Normal file
17
submodules/DatePickerNode/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
425
submodules/DatePickerNode/Sources/DatePickerNode.swift
Normal file
425
submodules/DatePickerNode/Sources/DatePickerNode.swift
Normal 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
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
56
submodules/InviteLinksUI/BUILD
Normal file
56
submodules/InviteLinksUI/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
428
submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
Normal file
428
submodules/InviteLinksUI/Sources/InviteLinkEditController.swift
Normal 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
|
||||
}
|
||||
124
submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift
Normal file
124
submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
619
submodules/InviteLinksUI/Sources/InviteLinkListController.swift
Normal file
619
submodules/InviteLinksUI/Sources/InviteLinkListController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
766
submodules/InviteLinksUI/Sources/InviteLinkViewController.swift
Normal file
766
submodules/InviteLinksUI/Sources/InviteLinkViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
504
submodules/InviteLinksUI/Sources/InviteLinksGridNode.swift
Normal file
504
submodules/InviteLinksUI/Sources/InviteLinksGridNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift
Normal file
241
submodules/InviteLinksUI/Sources/ItemListDatePickerItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ swift_library(
|
|||
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
|
||||
"//submodules/StatisticsUI:StatisticsUI",
|
||||
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
|
||||
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,7 +449,6 @@ private class PeersNearbyControllerImpl: ItemListController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public func peersNearbyController(context: AccountContext) -> ViewController {
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
var replaceTopControllerImpl: ((ViewController) -> Void)?
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
20
submodules/SectionHeaderItem/BUILD
Normal file
20
submodules/SectionHeaderItem/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
119
submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift
Normal file
119
submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -530,7 +530,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
|
|||
return .single(false)
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
return .single(hasTwoStepAuth)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_editaccount.pdf",
|
||||
"filename" : "ic_linkexpired.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Flame.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Flame.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_linkfire.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Link.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/Link.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_link.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Links/QrLogo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Links/QrLogo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_qrlogo.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "NavigationSettingsIcon.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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 |
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Group2.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_walletupdate.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Group.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SmallGem.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue