Merge branch 'experimental-2' of gitlab.com:peter-iakovlev/telegram-ios into experimental-2

This commit is contained in:
overtake 2021-02-01 15:42:35 +04:00
commit 1ffa585fa4
441 changed files with 38286 additions and 8756 deletions

View file

@ -202,19 +202,29 @@ public final class ChatPeekTimeout {
public final class ChatPeerNearbyData: Equatable {
public static func == (lhs: ChatPeerNearbyData, rhs: ChatPeerNearbyData) -> Bool {
return lhs.distance == rhs.distance
}
public let distance: Int32
public init(distance: Int32) {
self.distance = distance
}
}
public final class ChatGreetingData: Equatable {
public static func == (lhs: ChatGreetingData, rhs: ChatGreetingData) -> Bool {
if let lhsSticker = lhs.sticker, let rhsSticker = rhs.sticker, !lhsSticker.isEqual(to: rhsSticker) {
return false
} else if (lhs.sticker == nil) != (rhs.sticker == nil) {
return false
}
return lhs.distance == rhs.distance
return true
}
public let distance: Int32
public let sticker: TelegramMediaFile?
public init(distance: Int32, sticker: TelegramMediaFile?) {
self.distance = distance
public init(sticker: TelegramMediaFile?) {
self.sticker = sticker
}
}
@ -270,12 +280,13 @@ public final class NavigateToChatControllerParams {
public let activateMessageSearch: (ChatSearchDomain, String)?
public let peekData: ChatPeekTimeout?
public let peerNearbyData: ChatPeerNearbyData?
public let greetingData: ChatGreetingData?
public let animated: Bool
public let options: NavigationAnimationOptions
public let parentGroupId: PeerGroupId?
public let completion: (ChatController) -> Void
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping (ChatController) -> Void = { _ in }) {
self.navigationController = navigationController
self.chatController = chatController
self.chatLocationContextHolder = chatLocationContextHolder
@ -292,6 +303,7 @@ public final class NavigateToChatControllerParams {
self.activateMessageSearch = activateMessageSearch
self.peekData = peekData
self.peerNearbyData = peerNearbyData
self.greetingData = greetingData
self.animated = animated
self.options = options
self.parentGroupId = parentGroupId
@ -383,11 +395,13 @@ public struct ContactListAdditionalOption: Equatable {
public let title: String
public let icon: ContactListActionItemIcon
public let action: () -> Void
public let clearHighlightAutomatically: Bool
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void) {
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
self.title = title
self.icon = icon
self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically
}
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {

View file

@ -6,14 +6,21 @@ import Postbox
import TelegramCore
public struct ChatListNodeAdditionalCategory {
public enum Appearance {
case option
case action
}
public var id: Int
public var icon: UIImage?
public var title: String
public var appearance: Appearance
public init(id: Int, icon: UIImage?, title: String) {
public init(id: Int, icon: UIImage?, title: String, appearance: Appearance = .option) {
self.id = id
self.icon = icon
self.title = title
self.appearance = appearance
}
}

View file

@ -32,20 +32,29 @@ public struct ChatListNodePeersFilter: OptionSet {
public final class PeerSelectionControllerParams {
public let context: AccountContext
public let filter: ChatListNodePeersFilter
public let hasChatListSelector: Bool
public let hasContactSelector: Bool
public let hasGlobalSearch: Bool
public let title: String?
public let attemptSelection: ((Peer) -> Void)?
public let createNewGroup: (() -> Void)?
public let pretendPresentedInModal: Bool
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil) {
public init(context: AccountContext, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false) {
self.context = context
self.filter = filter
self.hasChatListSelector = hasChatListSelector
self.hasContactSelector = hasContactSelector
self.hasGlobalSearch = hasGlobalSearch
self.title = title
self.attemptSelection = attemptSelection
self.createNewGroup = createNewGroup
self.pretendPresentedInModal = pretendPresentedInModal
}
}
public protocol PeerSelectionController: ViewController {
var peerSelected: ((PeerId) -> Void)? { get set }
var peerSelected: ((Peer) -> Void)? { get set }
var inProgress: Bool { get set }
var customDismiss: (() -> Void)? { get set }
}

View file

@ -305,7 +305,7 @@ public protocol PresentationGroupCall: class {
func setFullSizeVideo(peerId: PeerId?)
func setCurrentAudioOutput(_ output: AudioSessionOutput)
func updateMuteState(peerId: PeerId, isMuted: Bool)
func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
func invitePeer(_ peerId: PeerId) -> Bool
func removedPeer(_ peerId: PeerId)
@ -314,6 +314,8 @@ public protocol PresentationGroupCall: class {
var incomingVideoSources: Signal<[PeerId: UInt32], NoError> { get }
func makeIncomingVideoView(source: UInt32, completion: @escaping (PresentationCallVideoView?) -> Void)
func loadMoreMembers(token: String)
}
public protocol PresentationCallManager: class {

View file

@ -72,8 +72,9 @@ public final class AnimatedStickerFrame {
public let bytesPerRow: Int
let index: Int
let isLastFrame: Bool
let totalFrames: Int
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool) {
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool, totalFrames: Int) {
self.data = data
self.type = type
self.width = width
@ -81,6 +82,7 @@ public final class AnimatedStickerFrame {
self.bytesPerRow = bytesPerRow
self.index = index
self.isLastFrame = isLastFrame
self.totalFrames = totalFrames
}
}
@ -255,7 +257,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
}
if let frameData = frameData, draw {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
} else {
return nil
}
@ -633,7 +635,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
self.currentFrame += 1
if draw {
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
} else {
var frameData = Data(count: self.bytesPerRow * self.height)
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
@ -643,7 +645,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
if let cache = self.cache {
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
}
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
}
} else {
return nil
@ -744,6 +746,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
private var reportedStarted = false
public var completed: (Bool) -> Void = { _ in }
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
@ -757,6 +760,8 @@ public final class AnimatedStickerNode: ASDisplayNode {
private var canDisplayFirstFrame: Bool = false
private var playbackMode: AnimatedStickerPlaybackMode = .loop
public var stopAtNearestLoop: Bool = false
private let playbackStatus = Promise<AnimatedStickerStatus>()
public var status: Signal<AnimatedStickerStatus, NoError> {
return self.playbackStatus.get()
@ -964,9 +969,17 @@ public final class AnimatedStickerNode: ASDisplayNode {
}
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .once = strongSelf.playbackMode {
stopNow = true
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true
@ -1041,9 +1054,17 @@ public final class AnimatedStickerNode: ASDisplayNode {
}
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .once = strongSelf.playbackMode {
stopNow = true
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true

View file

@ -13,6 +13,7 @@ import AccountContext
import Emoji
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
private let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
@ -79,10 +80,14 @@ private let savedMessagesColors: NSArray = [
UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor
]
public enum AvatarNodeExplicitIcon {
case phone
}
private enum AvatarNodeState: Equatable {
case empty
case peerAvatar(PeerId, [String], TelegramMediaImageRepresentation?)
case custom(letter: [String], explicitColorIndex: Int?)
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
}
private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
@ -91,8 +96,8 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
return true
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)):
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations
case let (.custom(lhsLetters, lhsIndex), .custom(rhsLetters, rhsIndex)):
return lhsLetters == rhsLetters && lhsIndex == rhsIndex
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
default:
return false
}
@ -105,6 +110,7 @@ private enum AvatarNodeIcon: Equatable {
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
case phoneIcon
}
public enum AvatarNodeImageOverride: Equatable {
@ -115,6 +121,7 @@ public enum AvatarNodeImageOverride: Equatable {
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
case phoneIcon
}
public enum AvatarNodeColorOverride {
@ -323,6 +330,9 @@ public final class AvatarNode: ASDisplayNode {
case .deletedIcon:
representation = nil
icon = .deletedIcon
case .phoneIcon:
representation = nil
icon = .phoneIcon
}
} else if peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage
@ -383,7 +393,7 @@ public final class AvatarNode: ASDisplayNode {
}
}
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil) {
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
var explicitIndex: Int?
if let explicitColor = explicitColor {
switch explicitColor {
@ -391,11 +401,16 @@ public final class AvatarNode: ASDisplayNode {
explicitIndex = 5
}
}
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex)
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
if updatedState != self.state {
self.state = updatedState
let parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
let parameters: AvatarNodeParameters
if let icon = icon, case .phone = icon {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
} else {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
}
self.displaySuspended = true
self.contents = nil
@ -456,6 +471,8 @@ public final class AvatarNode: ASDisplayNode {
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
if case .deletedIcon = parameters.icon {
colorsArray = grayscaleColors
} else if case .phoneIcon = parameters.icon {
colorsArray = grayscaleColors
} else if case .savedMessagesIcon = parameters.icon {
colorsArray = savedMessagesColors
} else if case .repliesIcon = parameters.icon {
@ -505,6 +522,15 @@ public final class AvatarNode: ASDisplayNode {
if let deletedIcon = deletedIcon {
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
}
} else if case .phoneIcon = parameters.icon {
let factor: CGFloat = 1.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let phoneIcon = phoneIcon {
context.draw(phoneIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - phoneIcon.size.width) / 2.0), y: floor((bounds.size.height - phoneIcon.size.height) / 2.0)), size: phoneIcon.size))
}
} else if case .savedMessagesIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)

View file

@ -1,5 +1,5 @@
load(
"//build-input/data:variables.bzl",
"@build_configuration//:variables.bzl",
"telegram_api_id",
"telegram_api_hash",
"telegram_app_center_id",

View file

@ -146,7 +146,7 @@ API_AVAILABLE(ios(10))
dataDict[@"device_token"] = [appToken base64EncodedStringWithOptions:0];
dataDict[@"device_token_type"] = @"voip";
}
float tzOffset = ([[NSTimeZone systemTimeZone] secondsFromGMT] / 3600.0);
float tzOffset = [[NSTimeZone systemTimeZone] secondsFromGMT];
dataDict[@"tz_offset"] = @((int)tzOffset);
if (signatureDict != nil) {
for (id<NSCopying> key in signatureDict.allKeys) {

View file

@ -25,6 +25,7 @@ swift_library(
"//submodules/MergeLists:MergeLists",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/ContextUI:ContextUI",
],
visibility = [
"//visibility:public",

View file

@ -13,12 +13,58 @@ import AccountContext
import AlertUI
import AppBundle
import LocalizedPeerData
import ContextUI
public enum CallListControllerMode {
case tab
case navigation
}
private final class DeleteAllButtonNode: ASDisplayNode {
private let pressed: () -> Void
let contentNode: ContextExtractedContentContainingNode
private let buttonNode: HighlightableButtonNode
private let titleNode: ImmediateTextNode
init(presentationData: PresentationData, pressed: @escaping () -> Void) {
self.pressed = pressed
self.contentNode = ContextExtractedContentContainingNode()
self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode()
super.init()
self.addSubnode(self.contentNode)
self.buttonNode.addSubnode(self.titleNode)
self.contentNode.contentNode.addSubnode(self.buttonNode)
self.titleNode.attributedText = NSAttributedString(string: presentationData.strings.Notification_Exceptions_DeleteAll, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationBar.accentTextColor)
//self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.pressed()
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let titleSize = self.titleNode.updateLayout(constrainedSize)
self.titleNode.frame = CGRect(origin: CGPoint(), size: titleSize)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: titleSize)
return titleSize
}
override public func layout() {
super.layout()
let size = self.bounds.size
self.contentNode.frame = CGRect(origin: CGPoint(), size: size)
self.contentNode.contentRect = CGRect(origin: CGPoint(), size: size)
}
}
public final class CallListController: ViewController {
private var controllerNode: CallListControllerNode {
return self.displayNode as! CallListControllerNode
@ -43,6 +89,7 @@ public final class CallListController: ViewController {
private var editingMode: Bool = false
private let createActionDisposable = MetaDisposable()
private let clearDisposable = MetaDisposable()
public init(context: AccountContext, mode: CallListControllerMode) {
self.context = context
@ -104,6 +151,7 @@ public final class CallListController: ViewController {
self.createActionDisposable.dispose()
self.presentationDataDisposable?.dispose()
self.peerViewDisposable.dispose()
self.clearDisposable.dispose()
}
private func updateThemeAndStrings() {
@ -167,6 +215,7 @@ public final class CallListController: ViewController {
switch strongSelf.mode {
case .tab:
strongSelf.navigationItem.setLeftBarButton(nil, animated: true)
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
case .navigation:
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
}
@ -175,8 +224,25 @@ public final class CallListController: ViewController {
case .tab:
if strongSelf.editingMode {
strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed))
var pressedImpl: (() -> Void)?
let buttonNode = DeleteAllButtonNode(presentationData: strongSelf.presentationData, pressed: {
pressedImpl?()
})
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode)
strongSelf.navigationItem.rightBarButtonItem?.setCustomAction({
pressedImpl?()
})
pressedImpl = { [weak self, weak buttonNode] in
guard let strongSelf = self, let buttonNode = buttonNode else {
return
}
strongSelf.deleteAllPressed(buttonNode: buttonNode)
}
//strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: strongSelf, action: #selector(strongSelf.deleteAllPressed))
} else {
strongSelf.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed))
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(strongSelf.presentationData.theme), style: .plain, target: self, action: #selector(strongSelf.callPressed))
}
case .navigation:
if strongSelf.editingMode {
@ -203,6 +269,89 @@ public final class CallListController: ViewController {
self.beginCallImpl()
}
@objc private func deleteAllPressed(buttonNode: DeleteAllButtonNode) {
var items: [ContextMenuItem] = []
let beginClear: (Bool) -> Void = { [weak self] forEveryone in
guard let strongSelf = self else {
return
}
var signal = clearCallHistory(account: strongSelf.context.account, forEveryone: forEveryone)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
self?.clearDisposable.set(nil)
}
strongSelf.clearDisposable.set((signal
|> deliverOnMainQueue).start(completed: {
}))
}
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForMe, textColor: .destructive, icon: { _ in
return nil
}, action: { _, f in
f(.default)
beginClear(false)
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.CallList_DeleteAllForEveryone, textColor: .destructive, icon: { _ in
return nil
}, action: { _, f in
f(.default)
beginClear(true)
})))
final class ExtractedContentSourceImpl: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = keepInPlace
self.blurBackground = blurBackground
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: self, sourceNode: buttonNode.contentNode, keepInPlace: false, blurBackground: false)), items: .single(items), reactionItems: [], gesture: nil)
self.presentInGlobalOverlay(contextController)
}
private func beginCallImpl() {
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }, displayCallIcons: true))
controller.navigationPresentation = .modal
@ -234,9 +383,25 @@ public final class CallListController: ViewController {
@objc func editPressed() {
self.editingMode = true
switch self.mode {
case .tab:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
var pressedImpl: (() -> Void)?
let buttonNode = DeleteAllButtonNode(presentationData: self.presentationData, pressed: {
pressedImpl?()
})
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: buttonNode)
self.navigationItem.rightBarButtonItem?.setCustomAction({
pressedImpl?()
})
pressedImpl = { [weak self, weak buttonNode] in
guard let strongSelf = self, let buttonNode = buttonNode else {
return
}
strongSelf.deleteAllPressed(buttonNode: buttonNode)
}
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.deleteAllPressed))
case .navigation:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
}
@ -251,6 +416,7 @@ public final class CallListController: ViewController {
switch self.mode {
case .tab:
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
case .navigation:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}

View file

@ -120,8 +120,8 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(peer, editing, isActive):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(_, theme):
@ -139,8 +139,8 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item
}), directionHint: entry.directionHint)
case let .displayTabInfo(_, text):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint)
case let .groupCall(peer, editing, isActive):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: editing, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .groupCall(peer, _, isActive):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListGroupCallItem(presentationData: presentationData, context: context, style: showSettings ? .blocks : .plain, peer: peer, isActive: isActive, editing: false, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .messageEntry(topMessage, messages, _, _, dateTimeFormat, editing, hasActiveRevealControls, displayHeader):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, context: context, style: showSettings ? .blocks : .plain, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, displayHeader: displayHeader, interaction: nodeInteraction), directionHint: entry.directionHint)
case let .holeEntry(_, theme):
@ -263,9 +263,49 @@ final class CallListControllerNode: ASDisplayNode {
}, openInfo: { [weak self] peerId, messages in
self?.openInfo(peerId, messages)
}, delete: { [weak self] messageIds in
if let strongSelf = self {
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
guard let strongSelf = self, let peerId = messageIds.first?.peerId else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let strongSelf = self, let peer = peer else {
return
}
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forEveryone).start()
}))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: messageIds, type: .forLocalPeer).start()
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
})
}, updateShowCallsTab: { [weak self] value in
if let strongSelf = self {
let _ = updateCallListSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, {

View file

@ -432,7 +432,7 @@ class CallListGroupCallItemNode: ItemListRevealOptionsItemNode {
transition.updateFrameAdditive(node: strongSelf.joinBackgroundNode, frame: CGRect(origin: CGPoint(), size: joinButtonFrame.size))
let _ = joinTitleApply()
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size))
transition.updateFrameAdditive(node: strongSelf.joinTitleNode, frame: CGRect(origin: CGPoint(x: floor((joinButtonSize.width - joinTitleLayout.size.width) / 2.0), y: floor((joinButtonSize.height - joinTitleLayout.size.height) / 2.0) + 1.0), size: joinTitleLayout.size))
let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))

View file

@ -226,7 +226,10 @@ func callListNodeEntriesForView(view: CallListView, groupCalls: [Peer], state: C
func countMeaningfulCallListEntries(_ entries: [CallListNodeEntry]) -> Int {
var count: Int = 0
for entry in entries {
if case .setting = entry.stableId {} else {
switch entry.stableId {
case .setting, .groupCall:
break
default:
count += 1
}
}

View file

@ -0,0 +1,227 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private final class ZoomWheelNodeDrawingState: NSObject {
let transition: CGFloat
let reverse: Bool
init(transition: CGFloat, reverse: Bool) {
self.transition = transition
self.reverse = reverse
super.init()
}
}
final class ZoomWheelNode: ASDisplayNode {
class State: Equatable {
let active: Bool
init(active: Bool) {
self.active = active
}
static func ==(lhs: State, rhs: State) -> Bool {
if lhs.active != rhs.active {
return false
}
return true
}
}
private class TransitionContext {
let startTime: Double
let duration: Double
let previousState: State
init(startTime: Double, duration: Double, previousState: State) {
self.startTime = startTime
self.duration = duration
self.previousState = previousState
}
}
private var animator: ConstantDisplayLinkAnimator?
private var hasState = false
private var state: State = State(active: false)
private var transitionContext: TransitionContext?
override init() {
super.init()
self.isOpaque = false
}
func update(state: State, animated: Bool) {
var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if self.state != state {
let previousState = self.state
self.state = state
if animated {
self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState)
}
self.updateAnimations()
self.setNeedsDisplay()
}
}
private func updateAnimations() {
var animate = false
let timestamp = CACurrentMediaTime()
if let transitionContext = self.transitionContext {
if transitionContext.startTime + transitionContext.duration < timestamp {
self.transitionContext = nil
} else {
animate = true
}
}
if animate {
let animator: ConstantDisplayLinkAnimator
if let current = self.animator {
animator = current
} else {
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateAnimations()
})
self.animator = animator
}
animator.isPaused = false
} else {
self.animator?.isPaused = true
}
self.setNeedsDisplay()
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionFraction: CGFloat = self.state.active ? 1.0 : 0.0
var reverse = false
if let transitionContext = self.transitionContext {
let timestamp = CACurrentMediaTime()
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
t = min(1.0, max(0.0, t))
if transitionContext.previousState.active != self.state.active {
transitionFraction = self.state.active ? t : 1.0 - t
reverse = transitionContext.previousState.active
}
}
return ZoomWheelNodeDrawingState(transition: transitionFraction, reverse: reverse)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
guard let parameters = parameters as? ZoomWheelNodeDrawingState else {
return
}
let color = UIColor(rgb: 0xffffff)
context.setFillColor(color.cgColor)
let clearLineWidth: CGFloat = 4.0
let lineWidth: CGFloat = 1.0 + UIScreenPixel
context.scaleBy(x: 2.5, y: 2.5)
context.translateBy(x: 4.0, y: 3.0)
let _ = try? drawSvgPath(context, path: "M14,8.335 C14.36727,8.335 14.665,8.632731 14.665,9 C14.665,11.903515 12.48064,14.296846 9.665603,14.626311 L9.665,16 C9.665,16.367269 9.367269,16.665 9,16.665 C8.666119,16.665 8.389708,16.418942 8.34221,16.098269 L8.335,16 L8.3354,14.626428 C5.519879,14.297415 3.335,11.90386 3.335,9 C3.335,8.632731 3.632731,8.335 4,8.335 C4.367269,8.335 4.665,8.632731 4.665,9 C4.665,11.394154 6.605846,13.335 9,13.335 C11.39415,13.335 13.335,11.394154 13.335,9 C13.335,8.632731 13.63273,8.335 14,8.335 Z ")
let _ = try? drawSvgPath(context, path: "M9,2.5 C10.38071,2.5 11.5,3.61929 11.5,5 L11.5,9 C11.5,10.380712 10.38071,11.5 9,11.5 C7.619288,11.5 6.5,10.380712 6.5,9 L6.5,5 C6.5,3.61929 7.619288,2.5 9,2.5 Z ")
context.translateBy(x: -4.0, y: -3.0)
if parameters.transition > 0.0 {
let startPoint: CGPoint
let endPoint: CGPoint
let origin = CGPoint(x: 9.0, y: 10.0 - UIScreenPixel)
let length: CGFloat = 17.0
if parameters.reverse {
startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition))
endPoint = CGPoint(x: origin.x + length, y: origin.y + length)
} else {
startPoint = origin
endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition)
}
context.setBlendMode(.clear)
context.setLineWidth(clearLineWidth)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
context.setBlendMode(.normal)
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.strokePath()
}
}
}
private class ButtonNode: HighlightTrackingButtonNode {
private let backgroundNode: ASDisplayNode
private let textNode: ImmediateTextNode
init() {
self.backgroundNode = ASDisplayNode()
self.textNode = ImmediateTextNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlight in
if let strongSelf = self {
}
}
}
func update() {
}
}
final class CameraZoomNode: ASDisplayNode {
private let wheelNode: ZoomWheelNode
private let backgroundNode: ASDisplayNode
override init() {
self.wheelNode = ZoomWheelNode()
self.backgroundNode = ASDisplayNode()
super.init()
self.addSubnode(self.wheelNode)
}
}

View file

@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatHistoryImportTasks",
module_name = "ChatHistoryImportTasks",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,15 @@
import Foundation
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
public enum ChatHistoryImportTasks {
public final class Context {
}
public static func importState(peerId: PeerId) -> Signal<Float?, NoError> {
return .single(nil)
}
}

View file

@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatImportUI",
module_name = "ChatImportUI",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Postbox:Postbox",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AppBundle:AppBundle",
"//third-party/ZipArchive:ZipArchive",
"//submodules/AccountContext:AccountContext",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/ChatHistoryImportTasks:ChatHistoryImportTasks",
"//submodules/MimeTypes:MimeTypes",
"//submodules/ConfettiEffect:ConfettiEffect",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,864 @@
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import PresentationDataUtils
import RadialStatusNode
import AnimatedStickerNode
import AppBundle
import ZipArchive
import MimeTypes
import ConfettiEffect
import TelegramUniversalVideoContent
import SolidRoundedButtonNode
private final class ProgressEstimator {
private var averageProgressPerSecond: Double = 0.0
private var lastMeasurement: (Double, Float)?
init() {
}
func update(progress: Float) -> Double? {
let timestamp = CACurrentMediaTime()
if let (lastTimestamp, lastProgress) = self.lastMeasurement {
if abs(lastProgress - progress) >= 0.01 || abs(lastTimestamp - timestamp) > 1.0 {
let immediateProgressPerSecond = Double(progress - lastProgress) / (timestamp - lastTimestamp)
let alpha: Double = 0.01
self.averageProgressPerSecond = alpha * immediateProgressPerSecond + (1.0 - alpha) * self.averageProgressPerSecond
self.lastMeasurement = (timestamp, progress)
}
} else {
self.lastMeasurement = (timestamp, progress)
}
//print("progress = \(progress)")
//print("averageProgressPerSecond = \(self.averageProgressPerSecond)")
if self.averageProgressPerSecond < 0.0001 {
return nil
} else {
let remainingProgress = Double(1.0 - progress)
let remainingTime = remainingProgress / self.averageProgressPerSecond
//print("remainingTime \(remainingTime)")
return remainingTime
}
}
}
private final class ImportManager {
enum ImportError {
case generic
case chatAdminRequired
case invalidChatType
case userBlocked
case limitExceeded
}
enum State {
case progress(totalBytes: Int, totalUploadedBytes: Int, totalMediaBytes: Int, totalUploadedMediaBytes: Int)
case error(ImportError)
case done
}
private let account: Account
private let archivePath: String?
private let entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
private var session: ChatHistoryImport.Session?
private let disposable = MetaDisposable()
private let totalBytes: Int
private let totalMediaBytes: Int
private let mainFileSize: Int
private var pendingEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
private var entryProgress: [String: (Int, Int)] = [:]
private var activeEntries: [String: Disposable] = [:]
private var stateValue: State {
didSet {
self.statePromise.set(.single(self.stateValue))
}
}
private let statePromise = Promise<State>()
var state: Signal<State, NoError> {
return self.statePromise.get()
}
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
self.account = account
self.archivePath = archivePath
self.entries = entries
self.pendingEntries = entries
self.mainFileSize = fileSize(mainFile.path) ?? 0
var totalMediaBytes = 0
for entry in self.entries {
self.entryProgress[entry.0.path] = (Int(entry.0.uncompressedSize), 0)
totalMediaBytes += Int(entry.0.uncompressedSize)
}
self.totalBytes = self.mainFileSize + totalMediaBytes
self.totalMediaBytes = totalMediaBytes
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: 0, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: 0)
Logger.shared.log("ChatImportScreen", "Requesting import session for \(peerId), media count: \(entries.count) with pending entries:")
for entry in entries {
Logger.shared.log("ChatImportScreen", " \(entry.1)")
}
self.disposable.set((ChatHistoryImport.initSession(account: self.account, peerId: peerId, file: mainFile, mediaCount: Int32(entries.count))
|> mapError { error -> ImportError in
switch error {
case .chatAdminRequired:
return .chatAdminRequired
case .invalidChatType:
return .invalidChatType
case .generic:
return .generic
case .userBlocked:
return .userBlocked
case .limitExceeded:
return .limitExceeded
}
}
|> deliverOnMainQueue).start(next: { [weak self] session in
guard let strongSelf = self else {
return
}
strongSelf.session = session
strongSelf.updateState()
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
strongSelf.failWithError(error)
}))
}
deinit {
self.disposable.dispose()
for (_, disposable) in self.activeEntries {
disposable.dispose()
}
}
private func updateProgress() {
if case .error = self.stateValue {
return
}
var totalUploadedMediaBytes = 0
for (_, entrySizes) in self.entryProgress {
totalUploadedMediaBytes += entrySizes.1
}
var totalUploadedBytes = totalUploadedMediaBytes
if let _ = self.session {
totalUploadedBytes += self.mainFileSize
}
self.stateValue = .progress(totalBytes: self.totalBytes, totalUploadedBytes: totalUploadedBytes, totalMediaBytes: self.totalMediaBytes, totalUploadedMediaBytes: totalUploadedMediaBytes)
}
private func failWithError(_ error: ImportError) {
self.stateValue = .error(error)
for (_, disposable) in self.activeEntries {
disposable.dispose()
}
}
private func complete() {
guard let session = self.session else {
self.failWithError(.generic)
return
}
self.disposable.set((ChatHistoryImport.startImport(account: self.account, session: session)
|> deliverOnMainQueue).start(error: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.failWithError(.generic)
}, completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.stateValue = .done
}))
}
private func updateState() {
guard let session = self.session else {
Logger.shared.log("ChatImportScreen", "updateState called with no session, ignoring")
return
}
if self.pendingEntries.isEmpty && self.activeEntries.isEmpty {
Logger.shared.log("ChatImportScreen", "updateState called with no pending and no active entries, completing")
self.complete()
return
}
if case .error = self.stateValue {
Logger.shared.log("ChatImportScreen", "updateState called after error, ignoring")
return
}
guard let archivePath = self.archivePath else {
Logger.shared.log("ChatImportScreen", "updateState called with empty arhivePath, ignoring")
return
}
while true {
if self.activeEntries.count >= 3 {
Logger.shared.log("ChatImportScreen", "updateState concurrent processing limit reached, stop searching")
break
}
if self.pendingEntries.isEmpty {
Logger.shared.log("ChatImportScreen", "updateState no more pending entries, stop searching")
break
}
let entry = self.pendingEntries.removeFirst()
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
let startTime = CACurrentMediaTime()
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
Logger.shared.log("ChatImportScreen", "[Done in \(CACurrentMediaTime() - startTime) s] Extract \(entry.0.path) to \(tempFile.path)")
subscriber.putNext(tempFile)
subscriber.putCompletion()
} else {
subscriber.putError(.generic)
}
return EmptyDisposable
}
let account = self.account
let uploadedEntrySignal: Signal<Float, ImportError> = unpackedFile
|> mapToSignal { tempFile -> Signal<Float, ImportError> in
let pathExtension = (entry.1 as NSString).pathExtension
var mimeType = "application/octet-stream"
if !pathExtension.isEmpty, let value = TGMimeTypeMap.mimeType(forExtension: pathExtension) {
mimeType = value
}
return ChatHistoryImport.uploadMedia(account: account, session: session, file: tempFile, fileName: entry.0.path, mimeType: mimeType, type: entry.2)
|> mapError { error -> ImportError in
switch error {
case .chatAdminRequired:
return .chatAdminRequired
case .generic:
return .generic
}
}
}
let disposable = MetaDisposable()
self.activeEntries[entry.1] = disposable
disposable.set((uploadedEntrySignal
|> deliverOnMainQueue).start(next: { [weak self] progress in
guard let strongSelf = self else {
return
}
if let (size, _) = strongSelf.entryProgress[entry.0.path] {
strongSelf.entryProgress[entry.0.path] = (size, Int(progress * Float(entry.0.uncompressedSize)))
strongSelf.updateProgress()
}
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
strongSelf.failWithError(error)
}, completed: { [weak self] in
guard let strongSelf = self else {
return
}
Logger.shared.log("ChatImportScreen", "updateState entry \(entry.1) has completed upload")
strongSelf.activeEntries.removeValue(forKey: entry.0.path)
strongSelf.updateState()
}))
}
}
}
public final class ChatImportActivityScreen: ViewController {
private final class Node: ViewControllerTracingNode {
private weak var controller: ChatImportActivityScreen?
private let context: AccountContext
private var presentationData: PresentationData
private let animationNode: AnimatedStickerNode
private let doneAnimationNode: AnimatedStickerNode
private let radialStatus: RadialStatusNode
private let radialCheck: RadialStatusNode
private let radialStatusBackground: ASImageNode
private let radialStatusText: ImmediateTextNode
private let progressText: ImmediateTextNode
private let statusText: ImmediateTextNode
private let statusButtonText: ImmediateTextNode
private let statusButton: HighlightableButtonNode
private let doneButton: SolidRoundedButtonNode
private var validLayout: (ContainerViewLayout, CGFloat)?
private let totalBytes: Int
private var state: ImportManager.State
private var videoNode: UniversalVideoNode?
private var feedback: HapticFeedback?
fileprivate var remainingAnimationSeconds: Double?
init(controller: ChatImportActivityScreen, context: AccountContext, totalBytes: Int, totalMediaBytes: Int) {
self.controller = controller
self.context = context
self.totalBytes = totalBytes
self.state = .progress(totalBytes: totalBytes, totalUploadedBytes: 0, totalMediaBytes: totalMediaBytes, totalUploadedMediaBytes: 0)
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.animationNode = AnimatedStickerNode()
self.doneAnimationNode = AnimatedStickerNode()
self.doneAnimationNode.isHidden = true
self.radialStatus = RadialStatusNode(backgroundNodeColor: .clear)
self.radialCheck = RadialStatusNode(backgroundNodeColor: .clear)
self.radialStatusBackground = ASImageNode()
self.radialStatusBackground.isUserInteractionEnabled = false
self.radialStatusBackground.displaysAsynchronously = false
self.radialStatusBackground.image = generateCircleImage(diameter: 180.0, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
self.radialStatusText = ImmediateTextNode()
self.radialStatusText.isUserInteractionEnabled = false
self.radialStatusText.displaysAsynchronously = false
self.radialStatusText.maximumNumberOfLines = 1
self.radialStatusText.isAccessibilityElement = false
self.progressText = ImmediateTextNode()
self.progressText.isUserInteractionEnabled = false
self.progressText.displaysAsynchronously = false
self.progressText.maximumNumberOfLines = 1
self.progressText.isAccessibilityElement = false
self.statusText = ImmediateTextNode()
self.statusText.textAlignment = .center
self.statusText.isUserInteractionEnabled = false
self.statusText.displaysAsynchronously = false
self.statusText.maximumNumberOfLines = 0
self.statusText.isAccessibilityElement = false
self.statusButtonText = ImmediateTextNode()
self.statusButtonText.isUserInteractionEnabled = false
self.statusButtonText.displaysAsynchronously = false
self.statusButtonText.maximumNumberOfLines = 1
self.statusButtonText.isAccessibilityElement = false
self.statusButton = HighlightableButtonNode()
self.doneButton = SolidRoundedButtonNode(title: self.presentationData.strings.ChatImportActivity_OpenApp, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
if let path = getAppBundle().path(forResource: "HistoryImport", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 190 * 2, height: 190 * 2, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
if let path = getAppBundle().path(forResource: "HistoryImportDone", ofType: "tgs") {
self.doneAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 190 * 2, height: 190 * 2, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.doneAnimationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationNode.isHidden = true
}
self.doneAnimationNode.visibility = false
}
self.addSubnode(self.animationNode)
self.addSubnode(self.doneAnimationNode)
self.addSubnode(self.radialStatusBackground)
self.addSubnode(self.radialStatus)
self.addSubnode(self.radialCheck)
self.addSubnode(self.radialStatusText)
self.addSubnode(self.progressText)
self.addSubnode(self.statusText)
self.addSubnode(self.statusButtonText)
self.addSubnode(self.statusButton)
self.addSubnode(self.doneButton)
self.statusButton.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
self.statusButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.statusButtonText.layer.removeAnimation(forKey: "opacity")
strongSelf.statusButtonText.alpha = 0.4
} else {
strongSelf.statusButtonText.alpha = 1.0
strongSelf.statusButtonText.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.animationNode.completed = { [weak self] stopped in
guard let strongSelf = self, stopped else {
return
}
strongSelf.animationNode.visibility = false
strongSelf.doneAnimationNode.visibility = true
strongSelf.doneAnimationNode.isHidden = false
}
self.animationNode.frameUpdated = { [weak self] index, totalCount in
guard let strongSelf = self else {
return
}
let remainingSeconds = Double(totalCount - index) / 60.0
strongSelf.remainingAnimationSeconds = remainingSeconds
strongSelf.controller?.updateProgressEstimation()
}
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black)
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
videoNode.alpha = 0.01
self.videoNode = videoNode
self.addSubnode(videoNode)
videoNode.canAttachContent = true
videoNode.play()
self.doneButton.pressed = { [weak self] in
guard let strongSelf = self, let controller = strongSelf.controller else {
return
}
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
let selector = NSSelectorFromString("openURL:")
let url = URL(string: "tg://localpeer?id=\(controller.peerId.toInt64())")!
application.perform(selector, with: url)
}
}
}
}
@objc private func statusButtonPressed() {
switch self.state {
case .done, .progress:
self.controller?.cancel()
case .error:
self.controller?.beginImport()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
let availableHeight = layout.size.height - navigationHeight
var iconSize = CGSize(width: 190.0, height: 190.0)
var radialStatusSize = CGSize(width: 186.0, height: 186.0)
var maxIconStatusSpacing: CGFloat = 46.0
var maxProgressTextSpacing: CGFloat = 33.0
var progressStatusSpacing: CGFloat = 14.0
var statusButtonSpacing: CGFloat = 19.0
var maxK: CGFloat = availableHeight / (iconSize.height + maxIconStatusSpacing + 30.0 + maxProgressTextSpacing + 320.0)
maxK = max(0.5, min(1.0, maxK))
iconSize.width = floor(iconSize.width * maxK)
iconSize.height = floor(iconSize.height * maxK)
radialStatusSize.width = floor(radialStatusSize.width * maxK)
radialStatusSize.height = floor(radialStatusSize.height * maxK)
maxIconStatusSpacing = floor(maxIconStatusSpacing * maxK)
maxProgressTextSpacing = floor(maxProgressTextSpacing * maxK)
progressStatusSpacing = floor(progressStatusSpacing * maxK)
statusButtonSpacing = floor(statusButtonSpacing * maxK)
var updateRadialBackround = false
if let width = self.radialStatusBackground.image?.size.width {
if abs(width - radialStatusSize.width) > 0.01 {
updateRadialBackround = true
}
} else {
updateRadialBackround = true
}
if updateRadialBackround {
self.radialStatusBackground.image = generateCircleImage(diameter: radialStatusSize.width, lineWidth: 6.0, color: self.presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.2))
}
let effectiveProgress: CGFloat
switch state {
case let .progress(totalBytes, totalUploadedBytes, _, _):
if totalBytes == 0 {
effectiveProgress = 1.0
} else {
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
}
case .error:
effectiveProgress = 0.0
case .done:
effectiveProgress = 1.0
}
self.radialStatusText.attributedText = NSAttributedString(string: "\(Int(effectiveProgress * 100.0))%", font: Font.with(size: floor(36.0 * maxK), design: .round, weight: .semibold), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
let radialStatusTextSize = self.radialStatusText.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.progressText.attributedText = NSAttributedString(string: "\(dataSizeString(Int(effectiveProgress * CGFloat(self.totalBytes)))) of \(dataSizeString(Int(1.0 * CGFloat(self.totalBytes))))", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
let progressTextSize = self.progressText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
switch self.state {
case .progress, .done:
self.statusButtonText.attributedText = NSAttributedString(string: self.presentationData.strings.Common_Done, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
case .error:
self.statusButtonText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_Retry, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemAccentColor)
}
let statusButtonTextSize = self.statusButtonText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
switch self.state {
case .progress:
self.statusText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_InProgress, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
case let .error(error):
let errorText: String
switch error {
case .chatAdminRequired:
errorText = self.presentationData.strings.ChatImportActivity_ErrorNotAdmin
case .invalidChatType:
errorText = self.presentationData.strings.ChatImportActivity_ErrorInvalidChatType
case .generic:
errorText = self.presentationData.strings.ChatImportActivity_ErrorGeneric
case .userBlocked:
errorText = self.presentationData.strings.ChatImportActivity_ErrorUserBlocked
case .limitExceeded:
errorText = self.presentationData.strings.ChatImportActivity_ErrorLimitExceeded
}
self.statusText.attributedText = NSAttributedString(string: errorText, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemDestructiveColor)
case .done:
self.statusText.attributedText = NSAttributedString(string: self.presentationData.strings.ChatImportActivity_Success, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
}
let statusTextSize = self.statusText.updateLayout(CGSize(width: layout.size.width - 16.0 * 2.0, height: .greatestFiniteMagnitude))
let contentHeight: CGFloat
var hideIcon = false
if case .compact = layout.metrics.heightClass, layout.size.width > layout.size.height {
hideIcon = true
contentHeight = progressTextSize.height + progressStatusSpacing + 160.0
} else {
contentHeight = iconSize.height + maxIconStatusSpacing + radialStatusSize.height + maxProgressTextSpacing + progressTextSize.height + progressStatusSpacing + 140.0
}
transition.updateAlpha(node: self.radialStatus, alpha: hideIcon ? 0.0 : 1.0)
transition.updateAlpha(node: self.radialStatusBackground, alpha: hideIcon ? 0.0 : 1.0)
switch self.state {
case .done:
break
default:
transition.updateAlpha(node: self.radialStatusText, alpha: hideIcon ? 0.0 : 1.0)
}
transition.updateAlpha(node: self.radialCheck, alpha: hideIcon ? 0.0 : 1.0)
transition.updateAlpha(node: self.animationNode, alpha: hideIcon ? 0.0 : 1.0)
transition.updateAlpha(node: self.doneAnimationNode, alpha: hideIcon ? 0.0 : 1.0)
let contentOriginY = navigationHeight + floor((layout.size.height - contentHeight) / 2.0)
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
self.animationNode.updateLayout(size: iconSize)
self.doneAnimationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
self.doneAnimationNode.updateLayout(size: iconSize)
self.radialStatus.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - radialStatusSize.width) / 2.0), y: contentOriginY + iconSize.height + maxIconStatusSpacing), size: radialStatusSize)
let checkSize: CGFloat = 130.0
self.radialCheck.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - checkSize) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
self.radialStatusBackground.frame = self.radialStatus.frame
self.radialStatusText.frame = CGRect(origin: CGPoint(x: self.radialStatus.frame.minX + floor((self.radialStatus.frame.width - radialStatusTextSize.width) / 2.0), y: self.radialStatus.frame.minY + floor((self.radialStatus.frame.height - radialStatusTextSize.height) / 2.0)), size: radialStatusTextSize)
self.progressText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressTextSize.width) / 2.0), y: hideIcon ? contentOriginY : (self.radialStatus.frame.maxY + maxProgressTextSpacing)), size: progressTextSize)
if case .progress = self.state {
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.maxY + progressStatusSpacing), size: statusTextSize)
self.statusButtonText.isHidden = true
self.statusButton.isHidden = true
self.doneButton.isHidden = true
self.progressText.isHidden = false
} else if case .error = self.state {
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
self.statusButtonText.isHidden = false
self.statusButton.isHidden = false
self.doneButton.isHidden = true
self.progressText.isHidden = true
} else {
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
self.statusButtonText.isHidden = false
self.statusButton.isHidden = false
self.doneButton.isHidden = true
self.progressText.isHidden = true
}/* else {
self.statusText.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusTextSize.width) / 2.0), y: self.progressText.frame.minY), size: statusTextSize)
self.statusButtonText.isHidden = true
self.statusButton.isHidden = true
self.doneButton.isHidden = false
self.progressText.isHidden = true
}*/
let buttonSideInset: CGFloat = 75.0
let buttonWidth = max(240.0, min(layout.size.width - buttonSideInset * 2.0, horizontalContainerFillingSizeForLayout(layout: layout, sideInset: buttonSideInset)))
let buttonHeight = self.doneButton.updateLayout(width: buttonWidth, transition: .immediate)
let doneButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing + 10.0), size: CGSize(width: buttonWidth, height: buttonHeight))
self.doneButton.frame = doneButtonFrame
let statusButtonTextFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusButtonTextSize.width) / 2.0), y: self.statusText.frame.maxY + statusButtonSpacing), size: statusButtonTextSize)
self.statusButtonText.frame = statusButtonTextFrame
self.statusButton.frame = statusButtonTextFrame.insetBy(dx: -10.0, dy: -10.0)
if isFirstLayout {
self.updateState(state: self.state, animated: false)
}
}
func transitionToDoneAnimation() {
self.animationNode.stopAtNearestLoop = true
}
func updateState(state: ImportManager.State, animated: Bool) {
var wasDone = false
if case .done = self.state {
wasDone = true
}
self.state = state
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate)
let effectiveProgress: CGFloat
switch state {
case let .progress(totalBytes, totalUploadedBytes, _, _):
if totalBytes == 0 {
effectiveProgress = 1.0
} else {
effectiveProgress = CGFloat(totalUploadedBytes) / CGFloat(totalBytes)
}
case .error:
effectiveProgress = 0.0
case .done:
effectiveProgress = 1.0
}
self.radialStatus.transitionToState(.progress(color: self.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: max(0.01, effectiveProgress), cancelEnabled: false, animateRotation: false), animated: animated, synchronous: true, completion: {})
if case .done = state {
self.radialCheck.transitionToState(.progress(color: .clear, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: false, synchronous: true, completion: {})
self.radialCheck.transitionToState(.check(self.presentationData.theme.list.itemAccentColor), animated: animated, synchronous: true, completion: {})
self.radialStatus.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.radialStatus.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
})
self.radialStatusBackground.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.radialStatusBackground.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
})
self.radialCheck.layer.animateScale(from: 1.0, to: 1.05, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.radialCheck.layer.animateScale(from: 1.05, to: 1.0, duration: 0.07, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, additive: false)
})
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
transition.updateAlpha(node: self.radialStatusText, alpha: 0.0)
if !wasDone {
self.view.addSubview(ConfettiView(frame: self.view.bounds))
if self.feedback == nil {
self.feedback = HapticFeedback()
}
self.feedback?.success()
}
}
}
}
}
private var controllerNode: Node {
return self.displayNode as! Node
}
private let context: AccountContext
private var presentationData: PresentationData
fileprivate let cancel: () -> Void
fileprivate var peerId: PeerId
private let archivePath: String?
private let mainEntry: TempBoxFile
private let totalBytes: Int
private let totalMediaBytes: Int
private let otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]
private var importManager: ImportManager?
private var progressEstimator: ProgressEstimator?
private var totalMediaProgress: Float = 0.0
private var beganCompletion: Bool = false
private let disposable = MetaDisposable()
private let progressDisposable = MetaDisposable()
override public var _presentedInModal: Bool {
get {
return true
} set(value) {
}
}
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, ChatHistoryImport.MediaType)]) {
self.context = context
self.cancel = cancel
self.peerId = peerId
self.archivePath = archivePath
self.mainEntry = mainEntry
self.otherEntries = otherEntries.map { entry -> (SSZipEntry, String, ChatHistoryImport.MediaType) in
return (entry.0, entry.1, entry.2)
}
let mainEntrySize = fileSize(self.mainEntry.path) ?? 0
var totalMediaBytes = 0
for entry in self.otherEntries {
totalMediaBytes += Int(entry.0.uncompressedSize)
}
self.totalBytes = mainEntrySize + totalMediaBytes
self.totalMediaBytes = totalMediaBytes
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: true, hideBadge: true))
self.title = self.presentationData.strings.ChatImportActivity_Title
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false)
self.attemptNavigation = { _ in
return false
}
self.beginImport()
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
application.isIdleTimerDisabled = true
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
self.progressDisposable.dispose()
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
application.isIdleTimerDisabled = false
}
}
@objc private func cancelPressed() {
self.cancel()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, context: self.context, totalBytes: self.totalBytes, totalMediaBytes: self.totalMediaBytes)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
}
private func beginImport() {
self.progressEstimator = ProgressEstimator()
self.beganCompletion = false
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
resolvedPeerId = convertGroupToSupergroup(account: self.context.account, peerId: self.peerId)
|> mapError { _ -> ImportManager.ImportError in
return .generic
}
} else {
resolvedPeerId = .single(self.peerId)
}
self.disposable.set((resolvedPeerId
|> deliverOnMainQueue).start(next: { [weak self] peerId in
guard let strongSelf = self else {
return
}
let importManager = ImportManager(account: strongSelf.context.account, peerId: peerId, mainFile: strongSelf.mainEntry, archivePath: strongSelf.archivePath, entries: strongSelf.otherEntries)
strongSelf.importManager = importManager
strongSelf.progressDisposable.set((importManager.state
|> deliverOnMainQueue).start(next: { state in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState(state: state, animated: true)
if case let .progress(_, _, totalMediaBytes, totalUploadedMediaBytes) = state {
let progress = Float(totalUploadedMediaBytes) / Float(totalMediaBytes)
strongSelf.totalMediaProgress = progress
}
}))
}, error: { [weak self] error in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState(state: .error(error), animated: true)
}))
}
fileprivate func updateProgressEstimation() {
if !self.beganCompletion, let progressEstimator = self.progressEstimator, let remainingAnimationSeconds = self.controllerNode.remainingAnimationSeconds {
if let remainingSeconds = progressEstimator.update(progress: self.totalMediaProgress) {
//print("remainingSeconds: \(remainingSeconds)")
//print("remainingAnimationSeconds + 1.0: \(remainingAnimationSeconds + 1.0)")
if remainingSeconds <= remainingAnimationSeconds + 1.0 {
self.beganCompletion = true
self.controllerNode.transitionToDoneAnimation()
}
}
}
}
}

View file

@ -25,6 +25,7 @@ public enum ChatListSearchItemHeaderType {
case groupMembers
case activeVoiceChats
case recentCalls
case orImportIntoAnExistingGroup
fileprivate func title(strings: PresentationStrings) -> String {
switch self {
@ -68,6 +69,8 @@ public enum ChatListSearchItemHeaderType {
return strings.CallList_ActiveVoiceChatsHeader
case .recentCalls:
return strings.CallList_RecentCallsHeader
case .orImportIntoAnExistingGroup:
return strings.ChatList_HeaderImportIntoAnExistingGroup
}
}
@ -113,6 +116,8 @@ public enum ChatListSearchItemHeaderType {
return .activeVoiceChats
case .recentCalls:
return .recentCalls
case .orImportIntoAnExistingGroup:
return .orImportIntoAnExistingGroup
}
}
}
@ -142,6 +147,7 @@ private enum ChatListSearchItemHeaderId: Int32 {
case groupMembers
case activeVoiceChats
case recentCalls
case orImportIntoAnExistingGroup
}
public final class ChatListSearchItemHeader: ListViewItemHeader {

View file

@ -58,6 +58,7 @@ swift_library(
"//submodules/ChatListFilterSettingsHeaderItem:ChatListFilterSettingsHeaderItem",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/StickerResources:StickerResources",
],
visibility = [
"//visibility:public",

View file

@ -16,6 +16,7 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
let context: AccountContext
let title: String
let image: UIImage?
let appearance: ChatListNodeAdditionalCategory.Appearance
let isSelected: Bool
let action: () -> Void
@ -29,7 +30,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
context: AccountContext,
title: String,
image: UIImage?,
appearance: ChatListNodeAdditionalCategory.Appearance,
isSelected: Bool,
header: ListViewItemHeader?,
action: @escaping () -> Void
) {
self.presentationData = presentationData
@ -37,10 +40,16 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
self.context = context
self.title = title
self.image = image
self.appearance = appearance
self.isSelected = isSelected
self.action = action
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
switch appearance {
case .option:
self.header = ChatListSearchItemHeader(type: .chatTypes, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
case .action:
self.header = header
}
}
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) {
@ -81,6 +90,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
}
public func selected(listView: ListView) {
if case .action = self.appearance {
listView.clearHighlightAnimated(true)
}
self.action()
}
@ -107,6 +119,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
} else {
last = true
}
} else if let _ = nextItem as? ChatListAdditionalCategoryItem {
} else {
last = true
}
} else {
last = true
@ -172,16 +187,37 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
return
/*super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)*/
if let item = self.item, case .action = item.appearance {
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
}
public func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
let reallyHighlighted = self.isHighlighted
let highlightProgress: CGFloat = 1.0
if reallyHighlighted {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
self.highlightedBackgroundNode.alpha = 0.0
}
self.highlightedBackgroundNode.layer.removeAllAnimations()
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
} else {
if self.highlightedBackgroundNode.supernode != nil {
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 1.0 - highlightProgress, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
}
}
}
public func asyncLayout() -> (_ item: ChatListAdditionalCategoryItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (Bool, Bool) -> Void)) {
@ -206,20 +242,29 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
let updatedSelectionNode: CheckNode?
let isSelected = item.isSelected
rightInset += 28.0
let selectionNode: CheckNode
if let current = currentSelectionNode {
selectionNode = current
updatedSelectionNode = selectionNode
if case .option = item.appearance {
rightInset += 28.0
let selectionNode: CheckNode
if let current = currentSelectionNode {
selectionNode = current
updatedSelectionNode = selectionNode
} else {
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
selectionNode.isUserInteractionEnabled = false
updatedSelectionNode = selectionNode
}
} else {
selectionNode = CheckNode(strokeColor: item.presentationData.theme.list.itemCheckColors.strokeColor, fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, style: .plain)
selectionNode.isUserInteractionEnabled = false
updatedSelectionNode = selectionNode
updatedSelectionNode = nil
}
var titleAttributedString: NSAttributedString?
let textColor = item.presentationData.theme.list.itemPrimaryTextColor
let textColor: UIColor
if case .action = item.appearance {
textColor = item.presentationData.theme.list.itemAccentColor
} else {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
}
titleAttributedString = NSAttributedString(string: item.title, font: titleFont, textColor: textColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -261,11 +306,12 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
strongSelf.avatarNode.image = item.image
strongSelf.topSeparatorNode.isHidden = true
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)))
if let image = item.image {
strongSelf.avatarNode.image = item.image
transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0 + floor((avatarDiameter - image.size.width) / 2.0), y: floor((nodeLayout.contentSize.height - image.size.width) / 2.0)), size: image.size))
}
let _ = titleApply()
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame.offsetBy(dx: revealOffset, dy: 0.0))

View file

@ -24,6 +24,7 @@ import LocalizedPeerData
import TelegramIntents
import TooltipUI
import TelegramCallsUI
import StickerResources
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
if listNode.scroller.isDragging {
@ -143,6 +144,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let featuredFiltersDisposable = MetaDisposable()
private var processedFeaturedFilters = false
private let preloadedSticker = Promise<TelegramMediaFile?>(nil)
private let preloadStickerDisposable = MetaDisposable()
private let isReorderingTabsValue = ValuePromise<Bool>(false)
private var searchContentNode: NavigationBarSearchContentNode?
@ -583,7 +587,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.deletePeerChat(peerId: peerId, joined: joined)
}
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, activateInput, promoInfo in
if let strongSelf = self {
if let navigationController = strongSelf.navigationController as? NavigationController {
var scrollToEndIfExists = false
@ -591,43 +595,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
scrollToEndIfExists = true
}
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
if let promoInfo = promoInfo {
switch promoInfo {
case .proxy:
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
let _ = (strongSelf.preloadedSticker.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] greetingSticker in
if let strongSelf = self {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
if let promoInfo = promoInfo {
switch promoInfo {
case .proxy:
let _ = (ApplicationSpecificNotice.getProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if !value {
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
}
})
case let .psa(type, _):
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if !value {
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
let key = "ChatList.PsaAlert.\(type)"
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
text = string
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
text = string
}
controller.displayPromoAnnouncement(text: text)
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
}
})
}
if !value {
controller.displayPromoAnnouncement(text: strongSelf.presentationData.strings.DialogList_AdNoticeAlert)
let _ = ApplicationSpecificNotice.setProxyAdsAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager).start()
}
})
case let .psa(type, _):
let _ = (ApplicationSpecificNotice.getPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id)
|> deliverOnMainQueue).start(next: { value in
guard let strongSelf = self else {
return
}
if !value {
var text = strongSelf.presentationData.strings.ChatList_GenericPsaAlert
let key = "ChatList.PsaAlert.\(type)"
if let string = strongSelf.presentationData.strings.primaryComponent.dict[key] {
text = string
} else if let string = strongSelf.presentationData.strings.secondaryComponent?.dict[key] {
text = string
}
controller.displayPromoAnnouncement(text: text)
let _ = ApplicationSpecificNotice.setPsaAcknowledgment(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peer.id).start()
}
})
}
}))
if activateInput {
strongSelf.prepareRandomGreetingSticker()
}
}
}))
})
}
}
}
@ -1077,6 +1091,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.displayNodeDidLoad()
}
public override func displayNodeDidLoad() {
super.displayNodeDidLoad()
Queue.mainQueue().after(1.0) {
self.prepareRandomGreetingSticker()
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@ -1212,8 +1234,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
switch controller.content {
case let .archivedChat(archivedChat):
if peerIds.contains(PeerId(archivedChat.peerId)) {
case let .archivedChat(peerId, _, _, _):
if peerIds.contains(PeerId(peerId)) {
controller.dismiss()
}
default:
@ -1960,7 +1982,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let signal: Signal<Void, NoError> = strongSelf.context.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: false)
removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: peerId.namespace == Namespaces.Peer.SecretChat)
}
}
|> afterDisposed {
@ -1970,6 +1992,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!)
@ -2117,6 +2148,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
canRemoveGlobally = true
}
} else if peer.peerId.namespace == Namespaces.Peer.SecretChat {
canRemoveGlobally = true
}
if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally {
@ -2126,43 +2159,89 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var items: [ActionSheetItem] = []
var canClear = true
var canStop = false
var canRemoveGlobally = false
var deleteTitle = strongSelf.presentationData.strings.Common_Delete
if let channel = chatPeer as? TelegramChannel {
if case .broadcast = channel.info {
canClear = false
deleteTitle = strongSelf.presentationData.strings.Channel_LeaveChannel
if channel.flags.contains(.isCreator) {
canRemoveGlobally = true
}
} else {
deleteTitle = strongSelf.presentationData.strings.Group_LeaveGroup
deleteTitle = strongSelf.presentationData.strings.Group_DeleteGroup
if channel.flags.contains(.isCreator) {
canRemoveGlobally = true
}
}
if let addressName = channel.addressName, !addressName.isEmpty {
canClear = false
}
} else if let group = chatPeer as? TelegramGroup {
if case .creator = group.role {
canRemoveGlobally = true
}
} else if let user = chatPeer as? TelegramUser, user.botInfo != nil {
canStop = !user.flags.contains(.isSupport)
canClear = user.botInfo == nil
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
} else if let _ = chatPeer as? TelegramSecretChat {
canClear = true
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
}
var canRemoveGlobally = false
let limitsConfiguration = strongSelf.context.currentLimitsConfiguration.with { $0 }
if chatPeer is TelegramUser && chatPeer.id != strongSelf.context.account.peerId {
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
canRemoveGlobally = true
}
} else if chatPeer is TelegramSecretChat {
canRemoveGlobally = true
}
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
if canClear {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
if canRemoveGlobally, (mainPeer is TelegramGroup || mainPeer is TelegramChannel) {
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .deleteAndLeave, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: false, completion: {
})
}))
let deleteForAllText: String
if let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribers
} else {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembers
}
items.append(ActionSheetButtonItem(title: deleteForAllText, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let deleteForAllConfirmation: String
if let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteChannelConfirmation
} else {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChannelInfo_DeleteGroupConfirmation
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
})
})
], parseMarkdown: true), in: .window(.root))
}))
} else {
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
if canClear {
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
guard let strongSelf = self else {
return
@ -2207,57 +2286,133 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}), in: .current)
}
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if joined || mainPeer.isDeleted {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
beginClear(.forEveryone)
actionSheet?.dismissAnimated()
}))
} else {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
beginClear(.forEveryone)
actionSheet?.dismissAnimated()
}))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
beginClear(.forLocalPeer)
actionSheet?.dismissAnimated()
}))
guard let strongSelf = self else {
return
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
} else {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
beginClear(.forLocalPeer)
})
], parseMarkdown: true), in: .window(.root))
}
}))
}
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
if chatPeer is TelegramSecretChat {
beginClear(.forEveryone)
} else {
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
if joined || mainPeer.isDeleted {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
beginClear(.forEveryone)
actionSheet?.dismissAnimated()
}))
} else {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
beginClear(.forLocalPeer)
actionSheet?.dismissAnimated()
}))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
beginClear(.forEveryone)
actionSheet?.dismissAnimated()
}))
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
} else {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
beginClear(.forLocalPeer)
})
], parseMarkdown: true), in: .window(.root))
}
}
}))
}
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
}))
if chatPeer is TelegramSecretChat {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
})
}))
} else {
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
if canRemoveGlobally, (mainPeer is TelegramGroup || mainPeer is TelegramChannel) {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .deleteAndLeave, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: false, completion: {
})
}))
let deleteForAllText: String
if let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribers
} else {
deleteForAllText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembers
}
items.append(ActionSheetButtonItem(title: deleteForAllText, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
return
}
let deleteForAllConfirmation: String
if let channel = mainPeer as? TelegramChannel, case .broadcast = channel.info {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllSubscribersConfirmationText
} else {
deleteForAllConfirmation = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText
}
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: deleteForAllConfirmation, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
})
})
], parseMarkdown: true), in: .window(.root))
}))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.present(actionSheet, in: .window(.root))
} else {
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
}
}))
}
}
if canStop {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
@ -2302,6 +2457,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let user = chatPeer as? TelegramUser, user.botInfo != nil {
canRemoveGlobally = false
}
if let _ = chatPeer as? TelegramSecretChat {
canRemoveGlobally = true
}
if canRemoveGlobally {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
@ -2318,6 +2476,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
completion(true)
}))
} else {
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
removed()
})
completion(true)
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let strongSelf = self else {
@ -2335,13 +2500,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
], parseMarkdown: true), in: .window(.root))
}))
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
removed()
})
completion(true)
}))
}
actionSheet.setItemGroups([
ActionSheetItemGroup(items: items),
@ -2524,6 +2682,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 {
@ -2566,6 +2731,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
private func prepareRandomGreetingSticker() {
let context = self.context
self.preloadedSticker.set(.single(nil)
|> then(randomGreetingSticker(account: context.account)
|> map { item in
return item?.file
}))
self.preloadStickerDisposable.set((self.preloadedSticker.get()
|> mapToSignal { sticker -> Signal<Void, NoError> in
if let sticker = sticker {
let _ = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: sticker)).start()
return chatMessageAnimationData(postbox: context.account.postbox, resource: sticker.resource, fitzModifier: nil, width: 384, height: 384, synchronousLoad: false)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
}).start())
}
override public func tabBarDisabledAction() {
self.donePressed()
}

View file

@ -475,8 +475,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in
self?.deletePeerChat?(peerId, joined)
}
itemNode.listNode.peerSelected = { [weak self] peerId, a, b in
self?.peerSelected?(peerId, a, b)
itemNode.listNode.peerSelected = { [weak self] peerId, animated, activateInput, promoInfo in
self?.peerSelected?(peerId, animated, activateInput, promoInfo)
}
itemNode.listNode.groupSelected = { [weak self] groupId in
self?.groupSelected?(groupId)
@ -522,7 +522,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
var toggleArchivedFolderHiddenByDefault: (() -> Void)?
var hidePsa: ((PeerId) -> Void)?
var deletePeerChat: ((PeerId, Bool) -> Void)?
var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
var peerSelected: ((Peer, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
var groupSelected: ((PeerGroupId) -> Void)?
var updatePeerGrouping: ((PeerId, Bool) -> Void)?
var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?

View file

@ -29,6 +29,7 @@ import GalleryData
import InstantPageUI
import ChatInterfaceState
import ShareController
import UndoUI
private enum ChatListTokenId: Int32 {
case filter
@ -684,7 +685,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
@ -896,9 +896,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}).start()
let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled]))
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in
peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peer in
let peerId = peer.id
if let strongSelf = self, let _ = peerSelectionController {
if peerId == strongSelf.context.account.peerId {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
(strongSelf.navigationController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messageIds.map { id -> EnqueueMessage in
return .forward(source: id, grouping: .auto, attributes: [])
})
@ -948,17 +952,25 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
})
}) |> deliverOnMainQueue).start(completed: {
if let strongSelf = self {
// strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
let controller = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))
controller.purposefulAction = { [weak self] in
self?.cancel?()
}
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
if let navigationController = strongSelf.navigationController, let peerSelectionControllerIndex = navigationController.viewControllers.firstIndex(where: { $0 is PeerSelectionController }) {
var viewControllers = navigationController.viewControllers
viewControllers.insert(controller, at: peerSelectionControllerIndex)
navigationController.setViewControllers(viewControllers, animated: false)
Queue.mainQueue().after(0.2) {
peerSelectionController?.dismiss()
}
})
} else {
strongSelf.navigationController?.pushViewController(controller, animated: false, completion: {
if let peerSelectionController = peerSelectionController {
peerSelectionController.dismiss()
}
})
}
strongSelf.updateState { state in
return state.withUpdatedSelectedMessageIds(nil)

View file

@ -26,6 +26,7 @@ import GalleryData
import AppBundle
import ShimmerEffect
import ChatListSearchRecentPeersNode
import UndoUI
private enum ChatListRecentEntryStableId: Hashable {
case topPeers
@ -429,7 +430,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .collapse:
actionTitle = strings.ChatList_Search_ShowLess
}
header = ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
let headerType: ChatListSearchItemHeaderType
if filter.contains(.onlyGroups) {
headerType = .chats
} else {
headerType = .localPeers
}
header = ChatListSearchItemHeader(type: headerType, theme: theme, strings: strings, actionTitle: actionTitle, action: actionTitle == nil ? nil : {
toggleExpandLocalResults()
})
}
@ -1744,6 +1751,35 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return
}
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
if let controller = strongSelf.navigationController?.topViewController as? ViewController {
var hasTooltip = false
controller.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
hasTooltip = true
controller.dismissWithCommitAction()
}
return true
})
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let slowdown = baseRate == .x1
controller.present(
UndoOverlayController(
presentationData: presentationData,
content: .audioRate(
slowdown: slowdown,
text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp
),
elevatedLayout: false,
animateInAsReplacement: hasTooltip,
action: { action in
return true
}
),
in: .current
)
}
})
}
mediaAccessoryPanel.togglePlayPause = { [weak self] in

View file

@ -234,7 +234,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
switch status {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
case .Local:
statusState = .none
case .Remote:

View file

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

View file

@ -479,11 +479,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case .groupReference:
return nil
case let .peer(peer):
guard let chatMainPeer = peer.peer.chatMainPeer else {
case let .peer(_, peer, combinedReadState, _, _, _, _, _, _, _, _, _):
guard let chatMainPeer = peer.chatMainPeer else {
return nil
}
return chatMainPeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
var result = ""
if item.context.account.peerId == chatMainPeer.id {
result += item.presentationData.strings.DialogList_SavedMessages
} else {
result += chatMainPeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
if let combinedReadState = combinedReadState, combinedReadState.count > 0 {
result += "\n\(item.presentationData.strings.VoiceOver_Chat_UnreadMessages(combinedReadState.count))"
}
return result
}
} set(value) {
}
@ -497,25 +506,25 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case .groupReference:
return nil
case let .peer(peer):
if let message = peer.messages.last {
case let .peer(messages, peer, combinedReadState, _, _, _, _, _, _, _, _, _):
if let message = messages.last {
var result = ""
if message.flags.contains(.Incoming) {
result += "Message"
result += item.presentationData.strings.VoiceOver_ChatList_Message
} else {
result += "Outgoing message"
result += item.presentationData.strings.VoiceOver_ChatList_OutgoingMessage
}
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: peer.messages, chatPeer: peer.peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: messages, chatPeer: peer, accountPeerId: item.context.account.peerId, isPeerGroup: false)
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
result += "\nFrom: \(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))"
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0)"
}
if !message.flags.contains(.Incoming), let combinedReadState = peer.combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
result += "\nRead"
if !message.flags.contains(.Incoming), let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
result += "\n\(item.presentationData.strings.VoiceOver_ChatList_MessageRead)"
}
result += "\n\(messageText)"
return result
} else {
return "Empty"
return item.presentationData.strings.VoiceOver_ChatList_MessageEmpty
}
}
} set(value) {
@ -958,7 +967,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} else if let message = messages.last, let author = message.author as? TelegramUser, let peer = itemPeer.chatMainPeer, !(peer is TelegramUser) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
} else if !displayAsMessage {
peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
peerText = authorSignature
} else {
peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
}
}
@ -1258,6 +1271,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIconOffset = 3.0
@ -1270,6 +1286,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIconOffset = 3.0
@ -1354,7 +1373,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
switch item.content {
case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _, displayAsMessage, _):
if !displayAsMessage {
if let peer = renderedPeer.peer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
if let peer = renderedPeer.chatMainPeer as? TelegramUser, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != item.context.account.peerId {
let updatedPresence = TelegramUserPresence(status: presence.status, lastActivity: 0)
let relativeStatus = relativeUserPresenceStatus(updatedPresence, relativeTo: timestamp)
if case .online = relativeStatus {

View file

@ -262,12 +262,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
}
default:
hideAuthor = true
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) {
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
messageText = text
}
}
case _ as TelegramMediaExpiredContent:
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) {
if let text = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId, forChatList: true) {
messageText = text
}
case let poll as TelegramMediaPoll:

View file

@ -165,13 +165,20 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch entry.entry {
case .HeaderEntry:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(_, id, title, image, selected, presentationData):
case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader?
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context,
title: title,
image: image,
appearance: appearance,
isSelected: selected,
header: header,
action: {
nodeInteraction.additionalCategorySelected(id)
}
@ -249,7 +256,14 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
switch mode {
case let .peers(_, _, additionalCategories, _):
if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
// TODO: hack, generalize
headerType = .orImportIntoAnExistingGroup
} else {
headerType = .chats
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
default:
break
@ -319,7 +333,14 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
switch mode {
case let .peers(_, _, additionalCategories, _):
if !additionalCategories.isEmpty {
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
// TODO: hack, generalize
headerType = .orImportIntoAnExistingGroup
} else {
headerType = .chats
}
header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
default:
break
@ -355,13 +376,20 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
case .HeaderEntry:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(index: _, id, title, image, selected, presentationData):
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader?
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context,
title: title,
image: image,
appearance: appearance,
isSelected: selected,
header: header,
action: {
nodeInteraction.additionalCategorySelected(id)
}
@ -424,7 +452,7 @@ public final class ChatListNode: ListView {
return _contentsReady.get()
}
public var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
public var peerSelected: ((Peer, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
public var disabledPeerSelected: ((Peer) -> Void)?
public var additionalCategorySelected: ((Int) -> Void)?
public var groupSelected: ((PeerGroupId) -> Void)?
@ -549,7 +577,7 @@ public final class ChatListNode: ListView {
}
}, peerSelected: { [weak self] peer, promoInfo in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer, true, promoInfo)
peerSelected(peer, true, true, promoInfo)
}
}, disabledPeerSelected: { [weak self] peer in
if let strongSelf = self, let disabledPeerSelected = strongSelf.disabledPeerSelected {
@ -578,7 +606,18 @@ public final class ChatListNode: ListView {
self?.additionalCategorySelected?(id)
}, messageSelected: { [weak self] peer, message, promoInfo in
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer, true, promoInfo)
var activateInput = false
for media in message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case .peerJoined, .groupCreated, .channelMigratedFromGroup, .historyCleared:
activateInput = true
default:
break
}
}
}
peerSelected(peer, true, activateInput, promoInfo)
}
}, groupSelected: { [weak self] groupId in
if let strongSelf = self, let groupSelected = strongSelf.groupSelected {
@ -1734,7 +1773,7 @@ public final class ChatListNode: ListView {
}
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: strongSelf.currentlyVisibleLatestChatListIndex() ?? .absoluteUpperBound, scrollPosition: .center(.top), animated: true, filter: strongSelf.chatListFilter)
strongSelf.setChatListLocation(location)
strongSelf.peerSelected?(peer, false, nil)
strongSelf.peerSelected?(peer, false, false, nil)
})
case .previous(unread: false), .next(unread: false):
var target: (ChatListIndex, Peer)? = nil
@ -1758,7 +1797,7 @@ public final class ChatListNode: ListView {
if let target = target {
let location: ChatListNodeLocation = .scroll(index: target.0, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: self.chatListFilter)
self.setChatListLocation(location)
self.peerSelected?(target.1, false, nil)
self.peerSelected?(target.1, false, false, nil)
}
case let .peerId(peerId):
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
@ -1768,7 +1807,7 @@ public final class ChatListNode: ListView {
guard let strongSelf = self, let peer = peer else {
return
}
strongSelf.peerSelected?(peer, false, nil)
strongSelf.peerSelected?(peer, false, false, nil)
})
case let .index(index):
guard index < 10 else {
@ -1787,7 +1826,7 @@ public final class ChatListNode: ListView {
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[10 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
self.setChatListLocation(location)
self.peerSelected?(renderedPeer.peer!, false, nil)
self.peerSelected?(renderedPeer.peer!, false, false, nil)
}
})
})

View file

@ -5,6 +5,7 @@ import TelegramCore
import SyncCore
import TelegramPresentationData
import MergeLists
import AccountContext
enum ChatListNodeEntryId: Hashable {
case Header
@ -50,7 +51,7 @@ enum ChatListNodeEntry: Comparable, Identifiable {
case HoleEntry(ChatListHole, theme: PresentationTheme)
case GroupReferenceEntry(index: ChatListIndex, presentationData: ChatListPresentationData, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], message: Message?, editing: Bool, unreadState: PeerGroupUnreadCountersCombinedSummary, revealed: Bool, hiddenByDefault: Bool)
case ArchiveIntro(presentationData: ChatListPresentationData)
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, selected: Bool, presentationData: ChatListPresentationData)
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
var sortIndex: ChatListNodeEntrySortIndex {
switch self {
@ -242,8 +243,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsSelected, lhsPresentationData):
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsSelected, rhsPresentationData) = rhs {
case let .AdditionalCategory(lhsIndex, lhsId, lhsTitle, lhsImage, lhsAppearance, lhsSelected, lhsPresentationData):
if case let .AdditionalCategory(rhsIndex, rhsId, rhsTitle, rhsImage, rhsAppearance, rhsSelected, rhsPresentationData) = rhs {
if lhsIndex != rhsIndex {
return false
}
@ -256,6 +257,9 @@ enum ChatListNodeEntry: Comparable, Identifiable {
if lhsImage !== rhsImage {
return false
}
if lhsAppearance != rhsAppearance {
return false
}
if lhsSelected != rhsSelected {
return false
}
@ -374,7 +378,7 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
_) = mode {
var index = 0
for category in additionalCategories.reversed(){
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
}
}

View file

@ -459,14 +459,14 @@ private class ChatListStatusProgressNode: ChatListStatusContentNode {
super.init()
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false))
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true))
self.addSubnode(self.statusNode)
}
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
if case let .progress(color, progress) = state {
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false), animated: animated, completion: {})
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
}
}

View file

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

View file

@ -279,7 +279,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
isCompact = true
originY = -1.0 - UIScreenPixel
case .compactFetching:
state = .progress(color: .white, lineWidth: nil, value: 0.0, cancelEnabled: true)
state = .progress(color: .white, lineWidth: nil, value: 0.0, cancelEnabled: true, animateRotation: true)
isCompact = true
originY = -1.0
}

View file

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

View file

@ -44,13 +44,13 @@ private final class ParticleLayer: CALayer {
}
}
final class ConfettiView: UIView {
public final class ConfettiView: UIView {
private var particles: [ParticleLayer] = []
private var displayLink: ConstantDisplayLinkAnimator?
private var localTime: Float = 0.0
override init(frame: CGRect) {
override public init(frame: CGRect) {
super.init(frame: frame)
self.isUserInteractionEnabled = false
@ -142,7 +142,7 @@ final class ConfettiView: UIView {
self.displayLink?.isPaused = false
}
required init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

View file

@ -158,7 +158,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ChatListSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
interaction.activateSearch()
})
case let .sort(theme, strings, sortOrder):
case let .sort(_, strings, sortOrder):
var text = strings.Contacts_SortedByName
if case .presence = sortOrder {
text = strings.Contacts_SortedByPresence
@ -166,17 +166,17 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .inline(dropDownIcon, .right), highlight: .alpha, header: nil, action: {
interaction.openSortMenu()
})
case let .permissionInfo(theme, title, text, suppressed):
case let .permissionInfo(_, title, text, suppressed):
return InfoListItem(presentationData: ItemListPresentationData(presentationData), title: title, text: .plain(text), style: .plain, closeAction: suppressed ? nil : {
interaction.suppressWarning()
})
case let .permissionEnable(theme, text):
case let .permissionEnable(_, text):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .none, header: nil, action: {
interaction.authorize()
})
case let .option(_, option, header, theme, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action)
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
case let .option(_, option, header, _, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action)
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer
var isContextActionEnabled = false
@ -928,9 +928,9 @@ public final class ContactListNode: ASDisplayNode {
|> mapToSignal { presentation in
var generateSections = false
var includeChatList = false
if case let .natural(natural) = presentation {
if case let .natural(_, includeChatListValue) = presentation {
generateSections = true
includeChatList = natural.includeChatList
includeChatList = includeChatListValue
}
if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation {

View file

@ -438,6 +438,10 @@ final class ContextActionsContainerNode: ASDisplayNode {
}
}
var hasAdditionalActions: Bool {
return self.additionalActionsNode != nil
}
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextController?, actionSelected: @escaping (ContextMenuActionResult) -> Void, feedbackTap: @escaping () -> Void, displayTextSelectionTip: Bool, blurBackground: Bool) {
self.blurBackground = blurBackground
self.shadowNode = ASImageNode()
@ -534,4 +538,14 @@ final class ContextActionsContainerNode: ASDisplayNode {
func animateIn() {
self.textSelectionTipNode?.animateIn()
}
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let additionalActionsNode = self.additionalActionsNode else {
return
}
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -1377,15 +1377,28 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
}
}
if let previousActionsContainerNode = previousActionsContainerNode {
if transition.isAnimated {
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1)
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
if transition.isAnimated {
if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions {
var initialFrame = self.actionsContainerNode.frame
let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height)
initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height
transition.animateFrame(node: self.actionsContainerNode, from: initialFrame)
transition.animatePosition(node: previousActionsContainerNode, to: CGPoint(x: 0.0, y: -delta), removeOnCompletion: false, additive: true)
previousActionsContainerNode.animateOut(offset: delta, transition: transition)
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1)
previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in
previousActionsContainerNode?.removeFromSupernode()
})
transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1)
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {

View file

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

View file

@ -0,0 +1,18 @@
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",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,388 @@
import Foundation
import Display
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import TelegramStringFormatting
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 disabledColor: UIColor
public let selectionColor: UIColor
public let selectedCurrentTextColor: UIColor
public let secondarySelectionColor: UIColor
public init(backgroundColor: UIColor, textColor: UIColor, secondaryTextColor: UIColor, accentColor: UIColor, disabledColor: UIColor, selectionColor: UIColor, selectedCurrentTextColor: UIColor, secondarySelectionColor: UIColor) {
self.backgroundColor = backgroundColor
self.textColor = textColor
self.secondaryTextColor = secondaryTextColor
self.accentColor = accentColor
self.disabledColor = disabledColor
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)
private let upperLimitDate = Date(timeIntervalSince1970: Double(Int32.max - 1))
private let dayFont = Font.regular(13.0)
private let dateFont = Font.with(size: 13.0, design: .regular, traits: .monospacedNumbers)
private let selectedDateFont = Font.bold(13.0)
private let calendar = Calendar(identifier: .gregorian)
private func monthForDate(_ date: Date) -> Date {
var components = calendar.dateComponents([.year, .month], from: date)
components.hour = 0
components.minute = 0
components.second = 0
return calendar.date(from: components)!
}
public final class DatePickerNode: ASDisplayNode, UIScrollViewDelegate {
class MonthNode: ASDisplayNode {
private let month: Date
var theme: DatePickerTheme {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var maximumDate: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var minimumDate: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
var date: Date? {
didSet {
if let size = self.validSize {
self.updateLayout(size: size)
}
}
}
private var validSize: CGSize?
private let selectionNode: ASImageNode
private let dateNodes: [ImmediateTextNode]
private let firstWeekday: Int
private let startWeekday: Int
private let numberOfDays: Int
init(theme: DatePickerTheme, month: Date, minimumDate: Date?, maximumDate: Date?, date: Date?) {
self.theme = theme
self.month = month
self.minimumDate = minimumDate
self.maximumDate = maximumDate
self.date = date
self.selectionNode = ASImageNode()
self.selectionNode.displaysAsynchronously = false
self.selectionNode.displayWithoutProcessing = true
self.dateNodes = (0..<42).map { _ in ImmediateTextNode() }
let components = calendar.dateComponents([.year, .month], from: month)
let startDayDate = calendar.date(from: components)!
self.firstWeekday = calendar.firstWeekday
self.startWeekday = calendar.dateComponents([.weekday], from: startDayDate).weekday!
self.numberOfDays = calendar.range(of: .day, in: .month, for: month)!.count
super.init()
self.addSubnode(self.selectionNode)
self.dateNodes.forEach { self.addSubnode($0) }
}
func updateLayout(size: CGSize) {
var weekday = self.firstWeekday
var started = false
var count = 0
for i in 0 ..< 42 {
let row: Int = Int(floor(Float(i) / 7.0))
let col: Int = i % 7
if !started && weekday == self.startWeekday {
started = true
}
if started {
count += 1
var isAvailableDate = true
if let minimumDate = self.minimumDate {
var components = calendar.dateComponents([.year, .month], from: self.month)
components.day = count
components.hour = 0
components.minute = 0
let date = calendar.date(from: components)!
if date < minimumDate {
isAvailableDate = false
}
}
if let maximumDate = self.maximumDate {
var components = calendar.dateComponents([.year, .month], from: self.month)
components.day = count
components.hour = 0
components.minute = 0
let date = calendar.date(from: components)!
if date > maximumDate {
isAvailableDate = false
}
}
var isSelectedDate = false
var isSelectedAndCurrentDate = false
let color: UIColor
if !isAvailableDate {
color = self.theme.disabledColor
} else if isSelectedAndCurrentDate {
color = .white
} else if isSelectedDate {
color = self.theme.accentColor
} else {
color = self.theme.textColor
}
let textNode = self.dateNodes[i]
textNode.attributedText = NSAttributedString(string: "\(count)", font: dateFont, textColor: color)
let textSize = textNode.updateLayout(size)
textNode.frame = CGRect(origin: CGPoint(x: CGFloat(col) * 20.0, y: CGFloat(row) * 20.0), size: textSize)
if count == self.numberOfDays {
break
}
}
}
}
}
struct State {
let minDate: Date
let maxDate: Date
let date: Date
let displayingMonthSelection: Bool
let selectedMonth: Date
}
private var state: State
private var theme: DatePickerTheme
private let strings: PresentationStrings
private let timeTitleNode: ImmediateTextNode
private let timeFieldNode: ASImageNode
private let monthButtonNode: HighlightTrackingButtonNode
private let monthTextNode: ImmediateTextNode
private let monthArrowNode: ASImageNode
private let previousButtonNode: HighlightableButtonNode
private let nextButtonNode: HighlightableButtonNode
private let dayNodes: [ImmediateTextNode]
private var previousMonthNode: MonthNode?
private var currentMonthNode: MonthNode?
private var nextMonthNode: MonthNode?
private let scrollNode: ASScrollNode
private var gestureRecognizer: UIPanGestureRecognizer?
private var gestureSelectedIndex: Int?
private var validLayout: CGSize?
public var maximumDate: Date? {
didSet {
}
}
public var minimumDate: Date = telegramReleaseDate {
didSet {
}
}
public var date: Date = Date() {
didSet {
guard self.date != oldValue else {
return
}
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
public init(theme: DatePickerTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
self.state = State(minDate: telegramReleaseDate, maxDate: upperLimitDate, date: Date(), displayingMonthSelection: false, selectedMonth: monthForDate(Date()))
self.timeTitleNode = ImmediateTextNode()
self.timeFieldNode = ASImageNode()
self.timeFieldNode.displaysAsynchronously = false
self.timeFieldNode.displayWithoutProcessing = true
self.monthButtonNode = HighlightTrackingButtonNode()
self.monthTextNode = ImmediateTextNode()
self.monthArrowNode = ASImageNode()
self.monthArrowNode.displaysAsynchronously = false
self.monthArrowNode.displayWithoutProcessing = true
self.previousButtonNode = HighlightableButtonNode()
self.nextButtonNode = HighlightableButtonNode()
self.dayNodes = (0..<7).map { _ in ImmediateTextNode() }
self.scrollNode = ASScrollNode()
super.init()
self.backgroundColor = theme.backgroundColor
self.addSubnode(self.monthTextNode)
self.addSubnode(self.monthArrowNode)
self.addSubnode(self.monthButtonNode)
self.addSubnode(self.previousButtonNode)
self.addSubnode(self.nextButtonNode)
self.addSubnode(self.scrollNode)
}
override public func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self
}
private func updateState(_ state: State, animated: Bool) {
self.state = state
if let size = self.validLayout {
self.updateLayout(size: size, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
}
public func updateTheme(_ theme: DatePickerTheme) {
guard theme != self.theme else {
return
}
self.theme = theme
self.backgroundColor = self.theme.backgroundColor
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.view.window?.endEditing(true)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let size = self.validLayout {
self.updateLayout(size: size, transition: .immediate)
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
let topInset: CGFloat = 60.0
let scrollSize = CGSize(width: size.width, height: size.height - topInset)
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: scrollSize)
self.scrollNode.view.contentSize = CGSize(width: scrollSize.width * 3.0, height: scrollSize.height)
self.scrollNode.view.contentOffset = CGPoint(x: scrollSize.width, y: 0.0)
for i in 0 ..< self.dayNodes.count {
let dayNode = self.dayNodes[i]
let day = Int32(i)
dayNode.attributedText = NSAttributedString(string: shortStringForDayOfWeek(strings: self.strings, day: day), font: dayFont, textColor: theme.secondaryTextColor)
let size = dayNode.updateLayout(size)
dayNode.frame = CGRect(origin: CGPoint(x: CGFloat(i) * 20.0, y: 0.0), size: size)
}
}
@objc private func monthButtonPressed(_ button: SegmentedControlItemNode) {
}
@objc private func previousButtonPressed(_ button: SegmentedControlItemNode) {
}
@objc private func nextButtonPressed(_ button: SegmentedControlItemNode) {
}
}

View file

@ -11,6 +11,7 @@ import AccountContext
public enum DeleteChatPeerAction {
case delete
case deleteAndLeave
case clearHistory
case clearCache
case clearCacheSuggestion
@ -57,7 +58,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
self.theme = theme
self.strings = strings
let peerFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
let textFont = Font.regular(floor(theme.baseFontSize * 14.0 / 17.0))
let boldFont = Font.semibold(floor(theme.baseFontSize * 14.0 / 17.0))
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isAccessibilityElement = false
@ -93,9 +95,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
case .clearCache, .clearCacheSuggestion:
switch action {
case .clearCache:
attributedText = NSAttributedString(string: strings.ClearCache_Description, font: peerFont, textColor: theme.primaryTextColor)
attributedText = NSAttributedString(string: strings.ClearCache_Description, font: textFont, textColor: theme.primaryTextColor)
case .clearCacheSuggestion:
attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: peerFont, textColor: theme.primaryTextColor)
attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: textFont, textColor: theme.primaryTextColor)
default:
break
}
@ -114,6 +116,18 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
} else {
text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
}
case .deleteAndLeave:
if chatPeer.id == context.account.peerId {
text = (strings.ChatList_DeleteSavedMessagesConfirmation, [])
} else if let chatPeer = chatPeer as? TelegramGroup {
text = strings.ChatList_DeleteAndLeaveGroupConfirmation(chatPeer.title)
} else if let chatPeer = chatPeer as? TelegramChannel {
text = strings.ChatList_DeleteAndLeaveGroupConfirmation(chatPeer.title)
} else if chatPeer is TelegramSecretChat {
text = strings.ChatList_DeleteSecretChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
} else {
text = strings.ChatList_DeleteChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
}
case .clearHistory:
text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder))
case .removeFromGroup:
@ -122,9 +136,9 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
break
}
if let text = text {
var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: peerFont, textColor: theme.primaryTextColor))
var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: textFont, textColor: theme.primaryTextColor))
for (_, range) in text.1 {
formattedAttributedText.addAttribute(.font, value: peerFont, range: range)
formattedAttributedText.addAttribute(.font, value: boldFont, range: range)
}
attributedText = formattedAttributedText
}

View file

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

View file

@ -33,7 +33,7 @@ public struct Font {
case bold
}
public static func with(size: CGFloat, design: Design = .regular, traits: Traits = []) -> UIFont {
public static func with(size: CGFloat, design: Design = .regular, weight: Weight = .regular, traits: Traits = []) -> UIFont {
if #available(iOS 13.0, *) {
let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor
var symbolicTraits = descriptor.symbolicTraits
@ -63,6 +63,15 @@ public struct Font {
default:
updatedDescriptor = updatedDescriptor?.withDesign(.default)
}
switch weight {
case .semibold:
let fontTraits = [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]
updatedDescriptor = updatedDescriptor?.addingAttributes([
UIFontDescriptor.AttributeName.traits: fontTraits
])
default:
break
}
if let updatedDescriptor = updatedDescriptor {
return UIFont(descriptor: updatedDescriptor, size: size)
} else {

View file

@ -497,6 +497,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
var nextItemOrigin = CGPoint(x: initialSpacing + itemInsets.left, y: 0.0)
var index = 0
var previousSection: GridSection?
var previousFillsRow = false
for item in self.items {
var itemSize = defaultItemSize
@ -508,6 +511,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
keepSection = false
}
if !previousFillsRow && item.fillsRowWithDynamicHeight != nil {
keepSection = false
}
previousFillsRow = item.fillsRowWithDynamicHeight != nil
if !keepSection {
if incrementedCurrentRow {
nextItemOrigin.x = initialSpacing + itemInsets.left

View file

@ -7,6 +7,22 @@ struct KeyboardSurface {
let host: UIView
}
public extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
static func currentFirst() -> UIResponder? {
Static.responder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return Static.responder
}
@objc private func _trap() {
Static.responder = self
}
}
private func getFirstResponder(_ view: UIView) -> UIView? {
if view.isFirstResponder {
return view

View file

@ -1351,6 +1351,9 @@ open class NavigationController: UINavigationController, ContainableController,
}
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let presentingViewController = self.presentingViewController {
presentingViewController.dismiss(animated: false, completion: nil)
}
if let controller = self.presentedViewController {
if flag {
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: {

View file

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

View file

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

View file

@ -139,7 +139,7 @@ public enum TabBarItemContextActionType {
}
open var navigationPresentation: ViewControllerNavigationPresentation = .default
var _presentedInModal: Bool = false
open var _presentedInModal: Bool = false
public var presentedOverCoveringView: Bool = false

View file

@ -3,9 +3,6 @@ import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public func qewfqewfq() {
}
private struct WindowLayout: Equatable {
let size: CGSize
let metrics: LayoutMetrics
@ -294,7 +291,7 @@ public class Window1 {
self.systemUserInterfaceStyle = hostView.systemUserInterfaceStyle
let boundsSize = self.hostView.eventView.bounds.size
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? defaultStatusBarHeight, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHost?.statusBarFrame.height ?? 0.0, onScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.statusBarHost = statusBarHost
let statusBarHeight: CGFloat
@ -303,7 +300,7 @@ public class Window1 {
self.keyboardManager = KeyboardManager(host: statusBarHost)
self.keyboardViewManager = KeyboardViewManager(host: statusBarHost)
} else {
statusBarHeight = self.deviceMetrics.statusBarHeight
statusBarHeight = 0.0
self.keyboardManager = nil
self.keyboardViewManager = nil
}
@ -406,7 +403,7 @@ public class Window1 {
self.overlayPresentationContext.containerLayoutUpdated(containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics), transition: .immediate)
self.statusBarChangeObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarFrameNotification, object: nil, queue: OperationQueue.main, using: { [weak self] notification in
if let strongSelf = self {
if let strongSelf = self, strongSelf.statusBarHost != nil {
let statusBarHeight: CGFloat = max(defaultStatusBarHeight, (notification.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? NSValue)?.cgRectValue.height ?? defaultStatusBarHeight)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .easeInOut)
@ -981,10 +978,12 @@ public class Window1 {
var statusBarHeight: CGFloat? = self.deviceMetrics.statusBarHeight(for: boundsSize)
if let statusBarHeightValue = statusBarHeight, let statusBarHost = self.statusBarHost {
statusBarHeight = max(statusBarHeightValue, statusBarHost.statusBarFrame.size.height)
} else {
statusBarHeight = nil
}
if self.deviceMetrics.type == .tablet, let onScreenNavigationHeight = self.hostView.onScreenNavigationHeight, onScreenNavigationHeight != self.deviceMetrics.onScreenNavigationHeight(inLandscape: false, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight) {
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? defaultStatusBarHeight, onScreenNavigationHeight: onScreenNavigationHeight)
self.deviceMetrics = DeviceMetrics(screenSize: UIScreen.main.bounds.size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight ?? 0.0, onScreenNavigationHeight: onScreenNavigationHeight)
}
let statusBarWasHidden = self.statusBarHidden

View file

@ -491,7 +491,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
var authorNameText: String?
if let author = message.effectiveAuthor {
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
authorNameText = authorSignature
} else if let author = message.effectiveAuthor {
authorNameText = author.displayTitle(strings: self.strings, displayOrder: self.nameOrder)
} else if let peer = message.peers[message.id.peerId] {
authorNameText = peer.displayTitle(strings: self.strings, displayOrder: self.nameOrder)

View file

@ -1225,7 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
if let (media, _) = mediaForMessage(message: message) {
if let _ = mediaForMessage(message: message) {
centralItemNode.activateAsInitial()
}
}

View file

@ -279,7 +279,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture
}
open func setControlsHidden(_ hidden: Bool, animated: Bool) {
guard self.areControlsHidden != hidden else {
guard self.areControlsHidden != hidden && (!self.isDismissed || hidden) else {
return
}
self.areControlsHidden = hidden

View file

@ -214,10 +214,10 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false

View file

@ -203,10 +203,10 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false

View file

@ -201,10 +201,10 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false

View file

@ -426,10 +426,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {})
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false

View file

@ -723,7 +723,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var fetching = false
if initialBuffering {
if displayProgress {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {})
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {})
} else {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
@ -740,7 +740,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
fetching = true
isPaused = true
}
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(progress), cancelEnabled: true, animateRotation: true)
default:
break
}

View file

@ -176,7 +176,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
case .Remote:
state = .download(.white)
default:

View file

@ -119,7 +119,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
case .Remote:
state = .download(.white)
default:

View file

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

View file

@ -0,0 +1,477 @@
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?, Bool)
case timeCustomPicker(PresentationTheme, Int32?)
case timeInfo(PresentationTheme, String)
case usageHeader(PresentationTheme, String)
case usagePicker(PresentationTheme, InviteLinkUsageLimit)
case usageCustomPicker(PresentationTheme, Int32?, Bool, 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, lhsActive):
if case let .timeExpiryDate(rhsTheme, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDate == rhsDate, lhsActive == rhsActive {
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, lhsCustomValue):
if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue {
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(theme, value, active):
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, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .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, customValue):
let text: String
if let value = value, value != 0 {
text = String(value)
} else {
text = 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, selectAllOnFocus: true, secondaryStyle: !customValue, 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, state.pickingTimeLimit))
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))
var customValue = false
if case .custom = state.usage {
customValue = true
}
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue))
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
var updating = false
}
public func inviteLinkEditController(context: AccountContext, peerId: PeerId, invite: ExportedInvitation?, completion: ((ExportedInvitation?) -> 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: .unlimited, time: .unlimited, 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)
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, nil)
})
let previousState = Atomic<InviteLinkEditControllerState?>(value: 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(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: {
updateState { state in
var updatedState = state
updatedState.updating = true
return updatedState
}
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 = 0
}
let usageLimit = state.usage.value
if invite == nil {
let _ = (createPeerExportedInvitation(account: context.account, peerId: peerId, expireDate: expireDate, usageLimit: usageLimit)
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?()
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
} else if let invite = invite {
let _ = (editPeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link, expireDate: expireDate, usageLimit: usageLimit)
|> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic))
|> deliverOnMainQueue).start(next: { invite in
completion?(invite)
dismissImpl?()
}, error: { _ in
updateState { state in
var updatedState = state
updatedState.updating = false
return updatedState
}
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
}
})
let previousState = previousState.swap(state)
var animateChanges = false
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit {
animateChanges = true
}
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: animateChanges)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let text: String
let sectionId: ItemListSectionId
init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId) {
self.theme = theme
self.text = text
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InviteLinkHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? InviteLinkHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class InviteLinkHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var animationNode: AnimatedStickerNode
private var item: InviteLinkHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.animationNode = AnimatedStickerNode()
if let path = getAppBundle().path(forResource: "Invite", ofType: "tgs") {
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
}
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: InviteLinkHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { item, params, neighbors in
let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.freeTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleLayout.size.width) / 2.0), y: topInset + 8.0), size: titleLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,743 @@
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
import UndoUI
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 header
case mainLink
case links(Int32)
case manage
}
private enum InviteLinkInviteEntry: Comparable, Identifiable {
case header(PresentationTheme, String, String)
case mainLink(PresentationTheme, ExportedInvitation)
case links(Int32, PresentationTheme, [ExportedInvitation])
case manage(PresentationTheme, String, Bool)
var stableId: InviteLinkInviteEntryId {
switch self {
case .header:
return .header
case .mainLink:
return .mainLink
case let .links(index, _, _):
return .links(index)
case .manage:
return .manage
}
}
static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsTitle, lhsText):
if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
return true
} else {
return false
}
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
}
case let .manage(lhsTheme, lhsText, lhsStandalone):
if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone {
return true
} else {
return false
}
}
}
static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool {
switch lhs {
case .header:
switch rhs {
case .header:
return false
case .mainLink, .links, .manage:
return true
}
case .mainLink:
switch rhs {
case .header, .mainLink:
return false
case .links, .manage:
return true
}
case let .links(lhsIndex, _, _):
switch rhs {
case .header, .mainLink:
return false
case let .links(rhsIndex, _, _):
return lhsIndex < rhsIndex
case .manage:
return true
}
case .manage:
switch rhs {
case .header, .mainLink, .links:
return false
case .manage:
return true
}
}
}
func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem {
switch self {
case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite)
}, shareAction: {
interaction.shareLink(invite)
}, contextAction: { node in
interaction.mainLinkContextAction(invite, node, nil)
}, viewAction: {
})
case let .links(_, _, invites):
return ItemListInviteLinkGridItem(presentationData: ItemListPresentationData(presentationData), invites: invites, share: true, sectionId: 1, style: .plain, tapAction: { invite in
interaction.copyLink(invite)
}, contextAction: { invite, _ in
interaction.shareLink(invite)
})
case let .manage(theme, text, standalone):
return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: {
interaction.manageLinks()
})
}
}
}
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 weak var parentNavigationController: NavigationController?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) {
self.context = context
self.peerId = peerId
self.parentNavigationController = parentNavigationController
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)
}
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?.presentingViewController?.dismiss(animated: false, completion: nil)
})
}
}
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 let invitesContext: PeerExportedInvitationsContext
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.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
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(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return 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((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> 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
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), 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?.parentNavigationController?.pushViewController(controller)
self?.controller?.dismiss()
})
let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil)
let peerView = context.account.postbox.peerView(id: peerId)
let invites: Signal<PeerExportedInvitationsState, NoError> = .single(PeerExportedInvitationsState())
self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, invites)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in
if let strongSelf = self {
var entries: [InviteLinkInviteEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
let mainInvite: ExportedInvitation?
if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation {
mainInvite = invite
} else {
mainInvite = nil
}
if let mainInvite = mainInvite {
entries.append(.mainLink(presentationData.theme, mainInvite))
}
// let additionalInvites = invites.invitations.filter { $0.link != mainInvite?.link }
// 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(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, additionalInvites.isEmpty))
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.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 = 409.0
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 - insets.bottom)
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 !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?
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer is DirectionalPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer
}
@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 currentOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 {
if currentOffset > 0.0 {
let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller)
if translation.y > 10.0 {
self.listNode.scroller.panGestureRecognizer.isEnabled = false
self.listNode.scroller.panGestureRecognizer.isEnabled = true
} else {
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
}
}
} else {
translation = 0.0
recognizer.setTranslation(CGPoint(), in: self.contentNode.view)
}
self.panGestureArguments = translation
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.contentNode.bounds = bounds
case .ended:
let translation = recognizer.translation(in: self.contentNode.view)
var velocity = recognizer.velocity(in: self.contentNode.view)
if case let .known(value) = contentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = contentOffset {
velocity = CGPoint()
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
self.panGestureArguments = nil
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.controller?.dismiss()
} else {
var bounds = self.contentNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .cancelled:
self.panGestureArguments = nil
let previousBounds = self.contentNode.bounds
var bounds = self.contentNode.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}
}
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
self.floatingHeaderOffset = offset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let controlsHeight: CGFloat = 44.0
let listTopInset = layoutTopInset + controlsHeight
let rawControlsOffset = offset + listTopInset - controlsHeight
let controlsOffset = max(layoutTopInset, rawControlsOffset)
let isOverscrolling = rawControlsOffset <= layoutTopInset
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
let previousFrame = self.headerNode.frame
if !controlsFrame.equalTo(previousFrame) {
self.headerNode.frame = controlsFrame
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y)
}
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
if !backgroundFrame.equalTo(previousBackgroundFrame) {
self.historyBackgroundNode.frame = backgroundFrame
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
}
}
}
}

View file

@ -0,0 +1,154 @@
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 title: String
let text: String
init(theme: PresentationTheme, title: String, text: String) {
self.theme = theme
self.title = title
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.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let textNode: TextNode
private let iconBackgroundNode: ASImageNode
private let iconNode: ASImageNode
private var item: InviteLinkInviteHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.iconBackgroundNode = ASImageNode()
self.iconBackgroundNode.displaysAsynchronously = false
self.iconBackgroundNode.displayWithoutProcessing = true
self.iconNode = ASImageNode()
self.iconNode.contentMode = .center
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.iconBackgroundNode)
self.addSubnode(self.iconNode)
}
func asyncLayout() -> (_ item: InviteLinkInviteHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentItem = self.item
return { item, params, neighbors in
let leftInset: CGFloat = 40.0 + params.leftInset
let topInset: CGFloat = 98.0
let spacing: CGFloat = 8.0
let bottomInset: CGFloat = 24.0
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, 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 attributedText = NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.freeTextColor)
let (textLayout, textApply) = makeTextLayout(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 + spacing + textLayout.size.height + bottomInset)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.accessibilityLabel = attributedText.string
if let _ = updatedTheme {
strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 92.0, color: item.theme.actionSheet.controlAccentColor)
strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/LargeLink"), color: item.theme.list.itemCheckColors.foregroundColor)
}
let iconSize = CGSize(width: 92.0, height: 92.0)
strongSelf.iconBackgroundNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.iconNode.frame = strongSelf.iconBackgroundNode.frame
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)
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textLayout.size.width) / 2.0), y: topInset + 8.0 + titleLayout.size.height + spacing), size: textLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,118 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import AppBundle
class InviteLinkInviteManageItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId = 0
let theme: PresentationTheme
let text: String
let standalone: Bool
let action: () -> Void
init(theme: PresentationTheme, text: String, standalone: Bool, action: @escaping () -> Void) {
self.theme = theme
self.text = text
self.standalone = standalone
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = InviteLinkInviteManageItemNode()
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? InviteLinkInviteManageItemNode 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.medium(23.0)
private let textFont = Font.regular(13.0)
class InviteLinkInviteManageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private var item: InviteLinkInviteManageItem?
init() {
self.backgroundNode = ASDisplayNode()
self.buttonNode = HighlightableButtonNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.item?.action()
}
func asyncLayout() -> (_ item: InviteLinkInviteManageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize = CGSize(width: params.width, height: 70.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.backgroundNode.backgroundColor = item.standalone ? .clear : item.theme.list.blocksBackgroundColor
strongSelf.buttonNode.setTitle(item.text, with: Font.regular(17.0), with: item.theme.actionSheet.controlAccentColor, for: .normal)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: 1000.0))
let size = strongSelf.buttonNode.measure(layout.contentSize)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.contentSize.width - size.width) / 2.0), y: floorToScreenPixels((layout.contentSize.height - size.height) / 2.0)), size: size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,671 @@
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
import UndoUI
private final class InviteLinkListControllerArguments {
let context: AccountContext
let shareMainLink: (ExportedInvitation) -> Void
let openMainLink: (ExportedInvitation) -> Void
let copyLink: (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, copyLink: @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.copyLink = copyLink
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], Int32, Bool)
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, lhsImportersCount, lhsIsPublic):
if case let .mainLink(rhsTheme, rhsInvite, rhsPeers, rhsImportersCount, rhsIsPublic) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, arePeerArraysEqual(lhsPeers, rhsPeers), lhsImportersCount == rhsImportersCount, lhsIsPublic == rhsIsPublic {
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, importersCount, isPublic):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
if let invite = invite {
arguments.copyLink(invite)
}
}, shareAction: {
if let invite = invite {
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, share: false, 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, share: false, 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]?, importers: PeerInvitationImportersState?) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
let mainInvite: ExportedInvitation?
var isPublic = false
if let peer = peerViewMainPeer(view), let address = peer.addressName, !address.isEmpty {
mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil)
isPublic = true
} else 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(.mainLinkHeader(presentationData.theme, isPublic ? presentationData.strings.InviteLink_PublicLink.uppercased() : presentationData.strings.InviteLink_PermanentLink.uppercased()))
let importersCount: Int32
if let count = importers?.count {
importersCount = count
} else if let count = mainInvite?.count {
importersCount = count
} else {
importersCount = 0
}
entries.append(.mainLink(presentationData.theme, mainInvite, importers?.importers.prefix(3).compactMap { $0.peer.peer } ?? [], importersCount, isPublic))
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)
var getControllerImpl: (() -> ViewController?)?
let invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: false, forceUpdate: false)
let revokedInvitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, revoked: true, forceUpdate: true)
let arguments = InviteLinkListControllerArguments(context: context, shareMainLink: { invite in
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
}, openMainLink: { invite in
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: nil, revokedInvitationsContext: revokedInvitesContext, importersContext: nil)
pushControllerImpl?(controller)
}, copyLink: { invite in
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, mainLinkContextAction: { 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?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return 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 controller = InviteLinkQRCodeController(context: context, invite: invite)
presentControllerImpl?(controller, nil)
})))
if invite.adminId.toInt64() != 0 {
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((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState { state in
var updatedState = state
updatedState.revokingPrivateLink = false
return updatedState
}
invitesContext.reload()
revokedInvitesContext.reload()
}))
}
})
]),
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: { invite in
if let invite = invite {
invitesContext.add(invite)
}
})
controller.navigationPresentation = .modal
pushControllerImpl?(controller)
}, openLink: { invite in
if let invite = invite {
let controller = InviteLinkViewController(context: context, peerId: peerId, invite: invite, invitationsContext: invitesContext, revokedInvitationsContext: revokedInvitesContext, 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?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
})))
if !invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextShare, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, 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 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: { invite in
if let invite = invite {
if invite.isRevoked {
invitesContext.remove(invite)
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
} else {
invitesContext.update(invite)
}
}
})
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.InviteLink_DeleteLinkAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
dismissAction()
revokeLinkDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
revokedInvitesContext.remove(invite)
})
]),
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: {
}))
invitesContext.remove(invite)
revokedInvitesContext.add(invite.withUpdated(isRevoked: true))
})
]),
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: {
}))
revokedInvitesContext.clear()
})
]),
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 .single(nil)
}
}
|> distinctUntilChanged
|> deliverOnMainQueue
|> map { invite -> PeerInvitationImportersContext? in
return invite.flatMap { PeerInvitationImportersContext(account: context.account, peerId: peerId, invite: $0) }
} |> afterNext { context in
if let context = context {
importersState.set(context.state |> map(Optional.init))
} else {
importersState.set(.single(nil))
}
}
let previousRevokedInvites = Atomic<PeerExportedInvitationsState?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state)
|> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previousRevokedInvites = previousRevokedInvites.swap(invites)
var crossfade = false
if (previousRevokedInvites?.hasLoadedOnce ?? false) != (revokedInvites.hasLoadedOnce) {
crossfade = true
}
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.invitations, revokedInvites: revokedInvites.invitations, importers: importers), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.didDisappear = { [weak controller] _ in
controller?.clearItemNodesHighlight(animated: true)
}
controller.visibleBottomContentOffsetChanged = { offset in
if case let .known(value) = offset, value < 40.0 {
}
}
pushControllerImpl = { [weak controller] c in
if let controller = controller {
(controller.navigationController as? NavigationController)?.pushViewController(c, animated: true)
}
}
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window(.root), with: p)
}
}
presentInGlobalOverlayImpl = { [weak controller] c in
if let controller = controller {
controller.presentInGlobalOverlay(c)
}
}
getControllerImpl = { [weak controller] in
return controller
}
return controller
}
final class InviteLinkContextExtractedContentSource: ContextExtractedContentSource {
var keepInPlace: Bool
let ignoreContentTouches: Bool = true
let blurBackground: Bool
private let controller: ViewController
private let sourceNode: ContextExtractedContentContainingNode
init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode) {
self.controller = controller
self.sourceNode = sourceNode
self.keepInPlace = true
self.blurBackground = false
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}

View file

@ -0,0 +1,413 @@
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(13.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])
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 + 16.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.maxY + 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 + 121.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: 18.0), size: cancelSize)
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
let buttonInset: CGFloat = 16.0
let doneButtonHeight = self.buttonNode.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: doneButtonHeight))
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
}
}
}

View file

@ -0,0 +1,870 @@
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
import UndoUI
class InviteLinkViewInteraction {
let context: AccountContext
let openPeer: (PeerId) -> Void
let copyLink: (ExportedInvitation) -> Void
let shareLink: (ExportedInvitation) -> Void
let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void
init(context: AccountContext, openPeer: @escaping (PeerId) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.openPeer = openPeer
self.copyLink = copyLink
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 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, Bool)
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, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsDate == rhsDate, lhsLoading == rhsLoading {
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 availability = invitationAvailability(invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite)
}, 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, loading):
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, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : 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)
}
private let titleFont = Font.bold(17.0)
private let subtitleFont = Font.with(size: 13, design: .regular, weight: .regular, traits: .monospacedNumbers)
private func textForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(minutes):\(secondsPadding)\(seconds)"
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
let minutesPadding = minutes < 10 ? "0" : ""
let seconds = value % 60
let secondsPadding = seconds < 10 ? "0" : ""
return "\(hours):\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
}
}
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 invitationsContext: PeerExportedInvitationsContext?
private let revokedInvitationsContext: PeerExportedInvitationsContext?
private let importersContext: PeerInvitationImportersContext?
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, peerId: PeerId, invite: ExportedInvitation, invitationsContext: PeerExportedInvitationsContext?, revokedInvitationsContext: PeerExportedInvitationsContext?, importersContext: PeerInvitationImportersContext?) {
self.context = context
self.peerId = peerId
self.invite = invite
self.invitationsContext = invitationsContext
self.revokedInvitationsContext = revokedInvitationsContext
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 let importersContext: PeerInvitationImportersContext
private var interaction: InviteLinkViewInteraction?
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var presentationDataDisposable: Disposable?
private var disposable: Disposable?
private let actionDisposable = MetaDisposable()
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 countdownTimer: SwiftSignalKit.Timer?
private var validLayout: ContainerViewLayout?
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 accentColor = presentationData.theme.actionSheet.controlAccentColor
self.editButton = HighlightableButtonNode()
self.editButton.setTitle(self.presentationData.strings.Common_Edit, with: Font.regular(17.0), with: accentColor, for: .normal)
self.doneButton = HighlightableButtonNode()
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: accentColor, 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))
}
}, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}, 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(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})))
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: { [weak self] _, 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.InviteLink_DeleteLinkAlert_Text),
ActionSheetButtonItem(title: presentationData.strings.InviteLink_DeleteLinkAlert_Action, color: .destructive, action: {
dismissAction()
self?.actionDisposable.set((deletePeerExportedInvitation(account: context.account, peerId: peerId, link: invite.link) |> deliverOnMainQueue).start(completed: {
}))
self?.controller?.revokedInvitationsContext?.remove(invite)
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
self?.controller?.present(controller, in: .window(.root))
})))
} else {
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 || (state.isLoadingMore && state.count > 0) {
entries.append(.importerHeader(presentationData.theme, presentationData.strings.InviteLink_PeopleJoined(Int32(state.count)).uppercased()))
}
var index: Int32 = 0
if state.importers.isEmpty && state.isLoadingMore {
let fakeUser = TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
for i in 0 ..< min(4, state.count) {
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, fakeUser, 0, true))
}
} else {
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, peer, importer.date, false))
}
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()
let invitationsContext = self.controller?.invitationsContext
let revokedInvitationsContext = self.controller?.revokedInvitationsContext
if let navigationController = navigationController {
let controller = inviteLinkEditController(context: self.context, peerId: self.peerId, invite: self.invite, completion: { invite in
if let invite = invite {
if invite.isRevoked {
invitationsContext?.remove(invite)
revokedInvitationsContext?.add(invite.withUpdated(isRevoked: true))
} else {
invitationsContext?.update(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: titleFont, textColor: self.presentationData.theme.actionSheet.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitleNode.attributedText?.string ?? "", font: subtitleFont, textColor: self.presentationData.theme.list.itemSecondaryTextColor)
let accentColor = self.presentationData.theme.actionSheet.controlAccentColor
self.editButton.setTitle(self.presentationData.strings.Common_Edit, with: Font.regular(17.0), with: accentColor, for: .normal)
self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: accentColor, 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))
var subtitleText = ""
if self.invite.isRevoked {
subtitleText = self.presentationData.strings.InviteLink_Revoked
} else if let usageLimit = self.invite.usageLimit, let count = self.invite.count, count >= usageLimit {
subtitleText = self.presentationData.strings.InviteLink_UsageLimitReached
} else if let expireDate = self.invite.expireDate {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime >= expireDate {
subtitleText = self.presentationData.strings.InviteLink_Expired
self.countdownTimer?.invalidate()
self.countdownTimer = nil
} else {
let elapsedTime = expireDate - currentTime
if elapsedTime >= 86400 {
subtitleText = self.presentationData.strings.InviteLink_ExpiresIn(timeIntervalString(strings: self.presentationData.strings, value: elapsedTime)).0
} else {
subtitleText = self.presentationData.strings.InviteLink_ExpiresIn(textForTimeout(value: elapsedTime)).0
if self.countdownTimer == nil {
let countdownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self, let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.countdownTimer = countdownTimer
countdownTimer.start()
}
}
}
}
self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.presentationData.theme.list.itemSecondaryTextColor)
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight))
let subtitleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - subtitleSize.width) / 2.0), y: 30.0 - UIScreenPixel), size: subtitleSize)
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
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: subtitleSize.height.isZero ? 18.0 : 10.0 + UIScreenPixel), 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 !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 currentOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 {
if currentOffset > 0.0 {
let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller)
if translation.y > 10.0 {
self.listNode.scroller.panGestureRecognizer.isEnabled = false
self.listNode.scroller.panGestureRecognizer.isEnabled = true
} else {
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
}
}
} else {
translation = 0.0
recognizer.setTranslation(CGPoint(), in: self.contentNode.view)
}
self.panGestureArguments = translation
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation
bounds.origin.y = min(0.0, bounds.origin.y)
self.contentNode.bounds = bounds
case .ended:
let translation = recognizer.translation(in: self.contentNode.view)
var velocity = recognizer.velocity(in: self.contentNode.view)
if case let .known(value) = contentOffset, value > 0.0 {
velocity = CGPoint()
} else if case .unknown = contentOffset {
velocity = CGPoint()
}
var bounds = self.contentNode.bounds
bounds.origin.y = -translation.y
bounds.origin.y = min(0.0, bounds.origin.y)
self.panGestureArguments = nil
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) {
self.controller?.dismiss()
} else {
var bounds = self.contentNode.bounds
let previousBounds = bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
case .cancelled:
self.panGestureArguments = nil
let previousBounds = self.contentNode.bounds
var bounds = self.contentNode.bounds
bounds.origin.y = 0.0
self.contentNode.bounds = bounds
self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
default:
break
}
}
private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let validLayout = self.validLayout else {
return
}
self.floatingHeaderOffset = offset
let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top)
let controlsHeight: CGFloat = 44.0
let listTopInset = layoutTopInset + controlsHeight
let rawControlsOffset = offset + listTopInset - controlsHeight
let controlsOffset = max(layoutTopInset, rawControlsOffset)
let isOverscrolling = rawControlsOffset <= layoutTopInset
let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight))
let previousFrame = self.headerNode.frame
if !controlsFrame.equalTo(previousFrame) {
self.headerNode.frame = controlsFrame
let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY)
transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y)
}
// transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0)
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height))
let previousBackgroundFrame = self.historyBackgroundNode.frame
if !backgroundFrame.equalTo(previousBackgroundFrame) {
self.historyBackgroundNode.frame = backgroundFrame
self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY)
transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y)
}
}
}
}

View file

@ -0,0 +1,667 @@
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 max(0.0, min(1.0, availability))
}
private enum ItemBackgroundColor: Equatable {
case blue
case green
case yellow
case red
case gray
var colors: (top: UIColor, bottom: UIColor, text: UIColor) {
switch self {
case .blue:
return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff))
case .green:
return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6))
case .yellow:
return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7))
case .red:
return (UIColor(rgb: 0xf2656a), UIColor(rgb: 0xf25f65), UIColor(rgb: 0xffd3de))
case .gray:
return (UIColor(rgb: 0xa8b2bb), UIColor(rgb: 0xa2abb4), UIColor(rgb: 0xe3e6e8))
}
}
}
private let moreIcon = 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)))
})
private let shareIcon = 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))
if let maskImage = UIImage(bundleImageName: "Chat/Links/Share") {
context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - maskImage.size.width) / 2.0), y: floorToScreenPixels((size.height - maskImage.size.height) / 2.0)), size: maskImage.size), mask: maskImage.cgImage!)
context.setBlendMode(.clear)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
}
})
private class ItemNode: ASDisplayNode {
private let selectionNode: HighlightTrackingButtonNode
private let wrapperNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let backgroundGradientLayer: CAGradientLayer
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 updateTimer: SwiftSignalKit.Timer?
private var params: (size: CGSize, wide: Bool, invite: ExportedInvitation, color: ItemBackgroundColor, presentationData: ItemListPresentationData)?
var action: (() -> Void)?
var contextAction: ((ASDisplayNode) -> Void)?
private let hapticFeedback = HapticFeedback()
override init() {
self.selectionNode = HighlightTrackingButtonNode()
self.wrapperNode = ASDisplayNode()
self.backgroundNode = ASDisplayNode()
self.backgroundNode.clipsToBounds = true
self.backgroundNode.cornerRadius = 15.0
if #available(iOS 13.0, *) {
self.backgroundNode.layer.cornerCurve = .continuous
}
self.backgroundNode.isUserInteractionEnabled = false
self.backgroundGradientLayer = CAGradientLayer()
self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.backgroundNode.layer.addSublayer(self.backgroundGradientLayer)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
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.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 2
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.wrapperNode)
self.wrapperNode.addSubnode(self.backgroundNode)
self.wrapperNode.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.wrapperNode.addSubnode(self.selectionNode)
self.wrapperNode.addSubnode(self.buttonNode)
self.wrapperNode.addSubnode(self.titleNode)
self.wrapperNode.addSubnode(self.subtitleNode)
self.selectionNode.addTarget(self, action: #selector(self.tapped), forControlEvents: .touchUpInside)
self.selectionNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 0.95)
} else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .linear)
transition.updateSublayerTransformScale(node: strongSelf, scale: 1.0)
}
}
}
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)
}
}
}
}
deinit {
self.updateTimer?.invalidate()
}
@objc private func tapped() {
self.hapticFeedback.impact(.light)
self.action?()
}
@objc private func buttonPressed() {
self.contextAction?(self.extractedContainerNode)
}
func update(size: CGSize, wide: Bool, share: Bool, invite: ExportedInvitation, presentationData: ItemListPresentationData, transition: ContainedViewLayoutTransition) -> CGSize {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let availability = invitationAvailability(invite)
let transitionFraction: CGFloat
let color: ItemBackgroundColor
let nextColor: ItemBackgroundColor
if invite.isRevoked {
color = .gray
nextColor = .gray
transitionFraction = 0.0
} else if invite.expireDate == nil && invite.usageLimit == nil {
color = .blue
nextColor = .blue
transitionFraction = 0.0
} else if availability >= 0.5 {
color = .green
nextColor = .yellow
transitionFraction = (availability - 0.5) / 0.5
} else if availability > 0.0 {
color = .yellow
nextColor = .red
transitionFraction = availability / 0.5
} else {
color = .red
nextColor = .red
transitionFraction = 0.0
}
let previousParams = self.params
self.params = (size, wide, invite, color, presentationData)
let previousExpireDate = previousParams?.invite.expireDate
if previousExpireDate != invite.expireDate {
self.updateTimer?.invalidate()
self.updateTimer = nil
if let expireDate = invite.expireDate, availability > 0.0 {
let timeout = min(2.0, max(0.001, Double(expireDate - currentTime)))
let updateTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: true, completion: { [weak self] in
if let strongSelf = self {
if let (size, wide, invite, _, presentationData) = strongSelf.params {
let _ = strongSelf.update(size: size, wide: wide, share: share, invite: invite, presentationData: presentationData, transition: .animated(duration: 0.3, curve: .linear))
}
}
}, queue: Queue.mainQueue())
self.updateTimer = updateTimer
updateTimer.start()
}
} else if availability.isZero {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let topColor = color.colors.top
let bottomColor = color.colors.bottom
let nextTopColor = nextColor.colors.top
let nextBottomColor = nextColor.colors.bottom
let colors: NSArray = [nextTopColor.mixedWith(topColor, alpha: transitionFraction).cgColor, nextBottomColor.mixedWith(bottomColor, alpha: transitionFraction).cgColor]
if let (_, _, previousInvite, previousColor, _) = previousParams, previousInvite == invite {
if previousColor != color && color == .red {
if let snapshotView = self.wrapperNode.view.snapshotContentTree() {
snapshotView.frame = self.wrapperNode.bounds
self.wrapperNode.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.backgroundGradientLayer.colors = colors as? [Any]
} else if (color == .green && nextColor == .yellow) || (color == .yellow && nextColor == .red) {
let previousColors = self.backgroundGradientLayer.colors
if transition.isAnimated {
self.backgroundGradientLayer.animate(from: previousColors as AnyObject, to: self.backgroundGradientLayer.colors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 2.5)
}
self.backgroundGradientLayer.colors = colors as? [Any]
}
} else {
self.backgroundGradientLayer.colors = colors as? [Any]
}
let secondaryTextColor = nextColor.colors.text.mixedWith(color.colors.text, alpha: transitionFraction)
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
self.buttonIconNode.image = share ? shareIcon : moreIcon
var subtitleText: String = ""
if let count = invite.count {
subtitleText = presentationData.strings.InviteLink_PeopleJoinedShort(count)
} else {
subtitleText = [.red, .gray].contains(color) ? 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 += ""
}
if share {
subtitleText = presentationData.strings.InviteLink_Expired
} else {
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 usageLimit = invite.usageLimit, let count = invite.count, count >= usageLimit {
if !subtitleText.isEmpty {
subtitleText += ""
}
if share {
subtitleText = presentationData.strings.InviteLink_UsageLimitReached
} else {
subtitleText += presentationData.strings.InviteLink_UsageLimitReached
}
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()
timerNode.isUserInteractionEnabled = false
self.timerNode = timerNode
self.addSubnode(timerNode)
}
timerNode.update(color: UIColor.white, creationTimestamp: invite.startDate ?? invite.date, deadlineTimestamp: expireDate)
if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy
}
} else {
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)
self.timerNode?.removeFromSupernode()
self.timerNode = nil
if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy
}
}
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)
self.subtitleNode.attributedText = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: secondaryTextColor)
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.wrapperNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
transition.updateFrame(layer: self.backgroundGradientLayer, 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], share: Bool, 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, share: share, 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 = max(0.0001, 1.0 - max(0.0, min(1.0, 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 sparks = fraction > 0.1 && fraction != 1.0
if sparks {
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()
if sparks {
for particle in self.particles {
let size: CGFloat = 2.0
context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
}
})
self.contentNode.contents = image?.cgImage
if let image = image {
self.contentNode.frame = CGRect(origin: CGPoint(), size: image.size)
}
if fraction <= .ulpOfOne {
self.animator?.invalidate()
self.animator = nil
} else {
if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.updateValues()
})
self.animator = animator
animator.isPaused = self.inHierarchyValue
}
}
}
}

View file

@ -0,0 +1,247 @@
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 var 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
super.init(layerBacked: false, dynamicBounce: false)
}
public override func didLoad() {
super.didLoad()
let datePicker = UIDatePicker()
datePicker.minimumDate = Date()
datePicker.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
datePicker.preferredDatePickerStyle = .inline
}
datePicker.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
self.view.addSubview(datePicker)
self.datePicker = datePicker
}
@objc private func datePickerUpdated() {
guard let datePicker = self.datePicker else {
return
}
self.item?.updated?(Int32(datePicker.date.timeIntervalSince1970))
}
public func asyncLayout() -> (_ item: ItemListDatePickerItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let height: CGFloat = 360.0
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors)
}
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
strongSelf.datePicker?.date = item.date.flatMap { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date()
strongSelf.datePicker?.frame = CGRect(origin: CGPoint(x: 16.0, y: 3.0), size: CGSize(width: contentSize.width - 32.0, height: contentSize.height))
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,409 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
enum InviteLinkTimeLimit: Equatable {
case hour
case day
case week
case unlimited
case custom(Int32)
init(position: Int) {
switch position {
case 0:
self = .hour
case 1:
self = .day
case 2:
self = .week
default:
self = .unlimited
}
}
var value: Int32? {
switch self {
case .hour:
return 3600
case .day:
return 86400
case .week:
return 604800
case .unlimited:
return nil
case let .custom(value):
return value
}
}
var position: Int {
switch self {
case .hour:
return 0
case .day:
return 1
case .week:
return 2
case .unlimited:
return 3
case let .custom(value):
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let relativeValue = value - currentTime
if relativeValue < 3600 {
return 0
} else if relativeValue < 86400 {
return 1
} else if relativeValue < 604800 {
return 2
} else {
return 3
}
}
}
}
final class ItemListInviteLinkTimeLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: InviteLinkTimeLimit
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (InviteLinkTimeLimit) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.enabled = enabled
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListInviteLinkTimeLimitItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListInviteLinkTimeLimitItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
private final class ItemListInviteLinkTimeLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lowTextNode: TextNode
private let mediumTextNode: TextNode
private let highTextNode: TextNode
private let unlimitedTextNode: TextNode
private let customTextNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: ItemListInviteLinkTimeLimitItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.lowTextNode = TextNode()
self.lowTextNode.isUserInteractionEnabled = false
self.lowTextNode.displaysAsynchronously = false
self.mediumTextNode = TextNode()
self.mediumTextNode.isUserInteractionEnabled = false
self.mediumTextNode.displaysAsynchronously = false
self.highTextNode = TextNode()
self.highTextNode.isUserInteractionEnabled = false
self.highTextNode.displaysAsynchronously = false
self.unlimitedTextNode = TextNode()
self.unlimitedTextNode.isUserInteractionEnabled = false
self.unlimitedTextNode.displaysAsynchronously = false
self.customTextNode = TextNode()
self.customTextNode.isUserInteractionEnabled = false
self.customTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lowTextNode)
self.addSubnode(self.mediumTextNode)
self.addSubnode(self.highTextNode)
self.addSubnode(self.unlimitedTextNode)
self.addSubnode(self.customTextNode)
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
if case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.value = CGFloat(item.value.position)
sliderView.isUserInteractionEnabled = item.enabled
sliderView.alpha = item.enabled ? 1.0 : 0.4
sliderView.layer.allowsGroupOpacity = !item.enabled
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value.position)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.startColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: ItemListInviteLinkTimeLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeLowTextLayout = TextNode.asyncLayout(self.lowTextNode)
let makeMediumTextLayout = TextNode.asyncLayout(self.mediumTextNode)
let makeHighTextLayout = TextNode.asyncLayout(self.highTextNode)
let makeUnlimitedTextLayout = TextNode.asyncLayout(self.unlimitedTextNode)
let makeCustomTextLayout = TextNode.asyncLayout(self.customTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let (lowTextLayout, lowTextApply) = makeLowTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 3600), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (mediumTextLayout, mediumTextApply) = makeMediumTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 86400), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (highTextLayout, highTextApply) = makeHighTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timeIntervalString(strings: item.strings, value: 604800), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (unlimitedTextLayout, unlimitedTextApply) = makeUnlimitedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.InviteLink_Create_TimeLimitNoLimit, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let customTextString: String
if case let .custom(value) = item.value {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let relativeValue = value - currentTime
if relativeValue > 0 {
customTextString = timeIntervalString(strings: item.strings, value: relativeValue)
} else {
customTextString = ""
}
} else {
customTextString = ""
}
let (customTextLayout, customTextApply) = makeCustomTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: customTextString, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0 //params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = lowTextApply()
let _ = mediumTextApply()
let _ = highTextApply()
let _ = unlimitedTextApply()
let _ = customTextApply()
var textNodes: [(TextNode, CGSize)] = [(strongSelf.lowTextNode, lowTextLayout.size),
(strongSelf.mediumTextNode, mediumTextLayout.size),
(strongSelf.highTextNode, highTextLayout.size),
(strongSelf.unlimitedTextNode, unlimitedTextLayout.size)]
if case .custom = item.value {
textNodes.insert((strongSelf.customTextNode, customTextLayout.size), at: item.value.position)
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value = InviteLinkTimeLimit(position: position)
self.item?.updated(value)
}
}

View file

@ -0,0 +1,263 @@
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]?
let share: Bool
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]?,
share: Bool,
sectionId: ItemListSectionId,
style: ItemListStyle,
tapAction: ((ExportedInvitation) -> Void)?,
contextAction: ((ExportedInvitation, ASDisplayNode) -> Void)?,
tag: ItemListItemTag? = nil
) {
self.presentationData = presentationData
self.invites = invites
self.share = share
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 topInset: CGFloat
if case .plain = item.style, case .otherSection = neighbors.top {
topInset = 16.0
} else {
topInset = 4.0
}
var height: CGFloat
let count = item.invites?.count ?? 0
if count > 0 {
if count % 2 == 0 {
height = topInset + 122.0 + 6.0
} else {
height = topInset + 102.0 + 6.0
}
} else {
height = 0.001
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.blocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.blocksBackgroundColor
insets = UIEdgeInsets()
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 += 10.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 ?? [], share: item.share, presentationData: item.presentationData, transition: .immediate)
strongSelf.gridNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topInset - 4.0), 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.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.bottomStripeNode.removeFromSupernode()
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width, height: contentSize.height))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = true
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,417 @@
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 value == 0 {
self = .unlimited
} else 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 0
case let .custom(value):
return value
}
}
var position: Int {
switch self {
case .low:
return 0
case .medium:
return 1
case .high:
return 2
case .unlimited:
return 3
case let .custom(value):
if value < 10 {
return 1
} else if value < 100 {
return 2
} else {
return 3
}
}
}
}
final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: InviteLinkUsageLimit
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (InviteLinkUsageLimit) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.enabled = enabled
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListInviteLinkUsageLimitItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ItemListInviteLinkUsageLimitItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
private final class ItemListInviteLinkUsageLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lowTextNode: TextNode
private let mediumTextNode: TextNode
private let highTextNode: TextNode
private let unlimitedTextNode: TextNode
private let customTextNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var item: ItemListInviteLinkUsageLimitItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.lowTextNode = TextNode()
self.lowTextNode.isUserInteractionEnabled = false
self.lowTextNode.displaysAsynchronously = false
self.mediumTextNode = TextNode()
self.mediumTextNode.isUserInteractionEnabled = false
self.mediumTextNode.displaysAsynchronously = false
self.highTextNode = TextNode()
self.highTextNode.isUserInteractionEnabled = false
self.highTextNode.displaysAsynchronously = false
self.unlimitedTextNode = TextNode()
self.unlimitedTextNode.isUserInteractionEnabled = false
self.unlimitedTextNode.displaysAsynchronously = false
self.customTextNode = TextNode()
self.customTextNode.isUserInteractionEnabled = false
self.customTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lowTextNode)
self.addSubnode(self.mediumTextNode)
self.addSubnode(self.highTextNode)
self.addSubnode(self.unlimitedTextNode)
self.addSubnode(self.customTextNode)
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
if case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.value = CGFloat(item.value.position)
sliderView.isUserInteractionEnabled = item.enabled
sliderView.alpha = item.enabled ? 1.0 : 0.4
sliderView.layer.allowsGroupOpacity = !item.enabled
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, case .custom = item.value {
sliderView.maximumValue = 3.0 + 1
sliderView.positionsCount = 4 + 1
} else {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
}
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value.position)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.startColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: ItemListInviteLinkUsageLimitItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeLowTextLayout = TextNode.asyncLayout(self.lowTextNode)
let makeMediumTextLayout = TextNode.asyncLayout(self.mediumTextNode)
let makeHighTextLayout = TextNode.asyncLayout(self.highTextNode)
let makeUnlimitedTextLayout = TextNode.asyncLayout(self.unlimitedTextNode)
let makeCustomTextLayout = TextNode.asyncLayout(self.customTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let (lowTextLayout, lowTextApply) = makeLowTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "1", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (mediumTextLayout, mediumTextApply) = makeMediumTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "10", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (highTextLayout, highTextApply) = makeHighTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "100", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (unlimitedTextLayout, unlimitedTextApply) = makeUnlimitedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.InviteLink_Create_UsersLimitNoLimit, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let customTextString: String
if case let .custom(value) = item.value {
customTextString = "\(value)"
} else {
customTextString = ""
}
let (customTextLayout, customTextApply) = makeCustomTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: customTextString, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0 //params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = lowTextApply()
let _ = mediumTextApply()
let _ = highTextApply()
let _ = unlimitedTextApply()
let _ = customTextApply()
var textNodes: [(TextNode, CGSize)] = [(strongSelf.lowTextNode, lowTextLayout.size),
(strongSelf.mediumTextNode, mediumTextLayout.size),
(strongSelf.highTextNode, highTextLayout.size),
(strongSelf.unlimitedTextNode, unlimitedTextLayout.size)]
if case .custom = item.value {
textNodes.insert((strongSelf.customTextNode, customTextLayout.size), at: item.value.position)
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value = InviteLinkUsageLimit(position: position)
self.item?.updated(value)
}
}

View file

@ -0,0 +1,495 @@
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 count: Int32
let peers: [Peer]
let displayButton: Bool
let displayImporters: Bool
let buttonColor: UIColor?
public let sectionId: ItemListSectionId
let style: ItemListStyle
let copyAction: (() -> Void)?
let shareAction: (() -> Void)?
let contextAction: ((ASDisplayNode) -> Void)?
let viewAction: (() -> Void)?
public let tag: ItemListItemTag?
public init(
context: AccountContext,
presentationData: ItemListPresentationData,
invite: ExportedInvitation?,
count: Int32,
peers: [Peer],
displayButton: Bool,
displayImporters: Bool,
buttonColor: UIColor?,
sectionId: ItemListSectionId,
style: ItemListStyle,
copyAction: (() -> Void)?,
shareAction: (() -> Void)?,
contextAction: ((ASDisplayNode) -> Void)?,
viewAction: (() -> Void)?,
tag: ItemListItemTag? = nil
) {
self.context = context
self.presentationData = presentationData
self.invite = invite
self.count = count
self.peers = peers
self.displayButton = displayButton
self.displayImporters = displayImporters
self.buttonColor = buttonColor
self.sectionId = sectionId
self.style = style
self.copyAction = copyAction
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 fieldButtonNode: HighlightTrackingButtonNode
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.fieldButtonNode = HighlightTrackingButtonNode()
self.addressButtonNode = HighlightTrackingButtonNode()
self.extractedContainerNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.addressButtonIconNode = ASImageNode()
self.addressButtonIconNode.contentMode = .center
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.fieldButtonNode)
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.fieldButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.addressNode.layer.removeAnimation(forKey: "opacity")
strongSelf.addressNode.alpha = 0.4
} else {
strongSelf.addressNode.alpha = 1.0
strongSelf.addressNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.fieldButtonNode.addTarget(self, action: #selector(self.fieldButtonPressed), forControlEvents: .touchUpInside)
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 fieldButtonPressed() {
if let item = self.item {
item.copyAction?()
}
}
@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.itemInputField.primaryColor
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: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 90.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitle: String
let subtitleColor: UIColor
if item.count > 0 {
subtitle = item.presentationData.strings.InviteLink_PeopleJoined(item.count)
subtitleColor = item.presentationData.theme.list.itemAccentColor
} 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:
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)
}
if !item.displayImporters {
height -= 57.0
}
if !item.displayButton {
height -= 63.0
}
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.itemInputField.backgroundColor)
strongSelf.addressButtonIconNode.image = actionButtonImage(color: item.presentationData.theme.list.itemInputField.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.fieldButtonNode.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 - 14.0, y: verticalInset), size: CGSize(width: 52.0, height: 52.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
strongSelf.shareButtonNode?.isHidden = !item.displayButton
strongSelf.avatarsButtonNode.isHidden = !item.displayImporters
strongSelf.avatarsNode.isHidden = !item.displayImporters
strongSelf.invitedPeersNode.isHidden = !item.displayImporters
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -376,6 +376,9 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
if peer.isScam {
credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 6.0
} else if peer.isFake {
credibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified {
credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.presentationData.theme)
}

View file

@ -22,17 +22,19 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
let icon: UIImage?
let title: String
public let alwaysPlain: Bool
let hasSeparator: Bool
let editing: Bool
let height: ItemListPeerActionItemHeight
let color: ItemListPeerActionItemColor
public let sectionId: ItemListSectionId
let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) {
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool, action: (() -> Void)?) {
self.presentationData = presentationData
self.icon = icon
self.title = title
self.alwaysPlain = alwaysPlain
self.hasSeparator = hasSeparator
self.editing = editing
self.height = height
self.color = color
@ -257,6 +259,8 @@ class ItemListPeerActionItemNode: ListViewItemNode {
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.bottomStripeNode.isHidden = strongSelf.bottomStripeNode.isHidden || !item.hasSeparator
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))

View file

@ -18,6 +18,7 @@ public enum ItemListDisclosureStyle {
public enum ItemListDisclosureLabelStyle {
case text
case detailText
case coloredText(UIColor)
case multilineDetailText
case badge(UIColor)
case color(UIColor)
@ -277,6 +278,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = detailFont
labelConstrain = params.width - params.rightInset - 40.0 - leftInset
case let .coloredText(color):
labelBadgeColor = color
labelFont = titleFont
default:
labelBadgeColor = item.presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont

View file

@ -29,6 +29,11 @@ public enum ItemListSingleLineInputClearType: Equatable {
}
}
public enum ItemListSingleLineInputAlignment {
case `default`
case right
}
public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let title: NSAttributedString
@ -36,10 +41,13 @@ 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
let enabled: Bool
let selectAllOnFocus: Bool
let secondaryStyle: Bool
public let sectionId: ItemListSectionId
let action: () -> Void
let textUpdated: (String) -> Void
@ -49,17 +57,20 @@ 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, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, 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
self.enabled = enabled
self.selectAllOnFocus = selectAllOnFocus
self.secondaryStyle = secondaryStyle
self.tag = tag
self.sectionId = sectionId
self.textUpdated = textUpdated
@ -174,7 +185,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
self.textNode.textField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
self.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
self.textNode.textField.accessibilityHint = item.placeholder
@ -209,6 +220,11 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
fontUpdated = true
}
var styleUpdated = false
if currentItem?.secondaryStyle != item.secondaryStyle {
styleUpdated = true
}
let leftInset: CGFloat = 16.0 + params.leftInset
var rightInset: CGFloat = 16.0 + params.rightInset
@ -243,13 +259,17 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor
strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
strongSelf.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
}
if fontUpdated {
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
}
if styleUpdated {
strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
}
let _ = titleApply()
@ -325,6 +345,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
}
@ -480,6 +507,13 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
self.item?.updatedFocus?(true)
if self.item?.selectAllOnFocus == true {
DispatchQueue.main.async {
let startPosition = self.textNode.textField.beginningOfDocument
let endPosition = self.textNode.textField.endOfDocument
self.textNode.textField.selectedTextRange = self.textNode.textField.textRange(from: startPosition, to: endPosition)
}
}
self.updateClearButtonVisibility()
}

View file

@ -12,6 +12,7 @@
@property (nonatomic, readonly) NSTimeInterval videoDuration;
@property (nonatomic, readonly) bool isAnimation;
@property (nonatomic, readonly) TGMediaAsset *originalAsset;
@property (nonatomic, readonly) CGSize dimensions;
- (instancetype)initWithURL:(NSURL *)url;

View file

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

View file

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

View file

@ -206,12 +206,6 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN
#pragma mark -
#pragma mark Movie processing
//- (void)enableSynchronizedEncodingUsingMovieWriter:(GPUImageMovieWriter *)movieWriter;
//{
// synchronizedMovieWriter = movieWriter;
// movieWriter.encodingLiveVideo = NO;
//}
- (void)startProcessing {
dispatch_sync(dispatch_get_main_queue(), ^{
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];

View file

@ -157,6 +157,11 @@
return _cachedSize;
}
- (CGSize)dimensions
{
return [self originalSize];
}
- (NSTimeInterval)videoDuration
{
return [self originalDuration];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -128,8 +128,8 @@ const NSTimeInterval TGPhotoQualityPreviewDuration = 15.0f;
CGSize dimensions = CGSizeZero;
if ([self.item isKindOfClass:[TGMediaAsset class]])
dimensions = ((TGMediaAsset *)self.item).dimensions;
// else if ([self.item isKindOfClass:[TGCameraCapturedVideo class]])
// dimensions = [((TGCameraCapturedVideo *)self.item).avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject.naturalSize;
else if ([self.item isKindOfClass:[TGCameraCapturedVideo class]])
dimensions = ((TGCameraCapturedVideo *)self.item).dimensions;
if (!CGSizeEqualToSize(dimensions, CGSizeZero))
_quality.maximumValue = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions] - 1;

View file

@ -28,7 +28,7 @@ private final class LegacyDataImportSplashImpl: WindowCoveringView, LegacyDataIm
self.updateLayout(size)
}
}
self.progressNode.transitionToState(.progress(color: self.theme?.list.itemAccentColor ?? UIColor(rgb: 0x007ee5), lineWidth: 2.0, value: CGFloat(max(0.025, self.progress.1)), cancelEnabled: false), animated: false, completion: {})
self.progressNode.transitionToState(.progress(color: self.theme?.list.itemAccentColor ?? UIColor(rgb: 0x007ee5), lineWidth: 2.0, value: CGFloat(max(0.025, self.progress.1)), cancelEnabled: false, animateRotation: true), animated: false, completion: {})
}
}

View file

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

View file

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

View file

@ -258,7 +258,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
transaction.updateMessage(id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType)
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var updatedMedia = currentMessage.media
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)

View file

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

View file

@ -479,7 +479,7 @@ private final class MediaPlayerContext {
self.state = .seeking(frameSource: frameSource, timestamp: timestamp, seekState: seekState, disposable: disposable, action: .play, enableSound: enableSound)
self.lastStatusUpdateTimestamp = nil
case let .paused(loadedState):
if faded {
if faded && false {
self.fadeTimer?.invalidate()
var volume: Double = 0.0
@ -674,7 +674,7 @@ private final class MediaPlayerContext {
self.state = .paused(loadedState)
self.lastStatusUpdateTimestamp = nil
if faded {
if faded && false {
self.fadeTimer?.invalidate()
var volume: Double = 1.0

View file

@ -317,7 +317,7 @@ private final class AudioPlayerRendererContext {
fileprivate func setVolume(_ volume: Double) {
if let mixerAudioUnit = self.mixerAudioUnit {
AudioUnitSetParameter(mixerAudioUnit,kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(volume), 0)
AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(volume), 0)
}
}

View file

@ -103,7 +103,7 @@ public final class MediaPlayerNode: ASDisplayNode {
self.currentRotationAngle = rotationAngle
self.currentAspect = aspect
var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
if !rotationAngle.isZero {
if abs(rotationAngle).remainder(dividingBy: Double.pi) > 0.1 {
transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect))
}
videoLayer.setAffineTransform(transform)

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