diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 50ab1a7250..f4eb9bd3d7 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -502,7 +502,11 @@ public func peersNearbyController(context: AccountContext) -> ViewController { }) |> mapToSignal { coordinate -> Signal in guard let coordinate = coordinate else { - let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil) + #if !DEBUG + #error("fix") + #endif + preconditionFailure() + /*let peersNearbyContext = PeersNearbyContext(network: context.account.network, stateManager: context.account.stateManager, coordinate: nil) return peersNearbyContext.get() |> map { peersNearby -> PeersNearbyData in var isVisible = false @@ -515,7 +519,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { } } return PeersNearbyData(latitude: 0.0, longitude: 0.0, address: nil, visible: isVisible, accountPeerId: context.account.peerId, users: [], groups: [], channels: []) - } + }*/ } return Signal { subscriber in diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index de39b38f92..587a74a9d9 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -369,6 +369,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode", "//submodules/TelegramUI/Components/LegacyInstantVideoController", "//submodules/TelegramUI/Components/FullScreenEffectView", + "//submodules/TelegramUI/Components/ShareWithPeersScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift b/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift index 2bf06f470c..9dd60d0b42 100644 --- a/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift +++ b/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift @@ -82,6 +82,8 @@ public final class RippleEffectView: MTKView { self.framebufferOnly = true self.isPaused = false + + self.isUserInteractionEnabled = false } public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index 27ed088b79..5bcc1de71f 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -28,8 +28,10 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/AnimatedCounterComponent", + "//submodules/TelegramUI/Components/TokenListTextField", "//submodules/AvatarNode", "//submodules/CheckNode", + "//submodules/PeerPresenceStatusManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ActionListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ActionListItemComponent.swift deleted file mode 100644 index 9e4fa31343..0000000000 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ActionListItemComponent.swift +++ /dev/null @@ -1,196 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import ComponentFlow -import MultilineTextComponent -import TelegramPresentationData - -final class ActionListItemComponent: Component { - let theme: PresentationTheme - let sideInset: CGFloat - let iconName: String? - let title: String - let hasNext: Bool - let action: () -> Void - - init( - theme: PresentationTheme, - sideInset: CGFloat, - iconName: String?, - title: String, - hasNext: Bool, - action: @escaping () -> Void - ) { - self.theme = theme - self.sideInset = sideInset - self.iconName = iconName - self.title = title - self.hasNext = hasNext - self.action = action - } - - static func ==(lhs: ActionListItemComponent, rhs: ActionListItemComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.sideInset != rhs.sideInset { - return false - } - if lhs.iconName != rhs.iconName { - return false - } - if lhs.title != rhs.title { - return false - } - if lhs.hasNext != rhs.hasNext { - return false - } - return true - } - - final class View: UIView { - private let containerButton: HighlightTrackingButton - - private let title = ComponentView() - private let iconView: UIImageView - private let separatorLayer: SimpleLayer - - private var highlightBackgroundFrame: CGRect? - private var highlightBackgroundLayer: SimpleLayer? - - private var component: ActionListItemComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - self.separatorLayer = SimpleLayer() - - self.containerButton = HighlightTrackingButton() - - self.iconView = UIImageView() - - super.init(frame: frame) - - self.layer.addSublayer(self.separatorLayer) - self.addSubview(self.containerButton) - - self.containerButton.addSubview(self.iconView) - - self.containerButton.highligthedChanged = { [weak self] isHighlighted in - guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { - return - } - - if isHighlighted { - self.superview?.bringSubviewToFront(self) - - let highlightBackgroundLayer: SimpleLayer - if let current = self.highlightBackgroundLayer { - highlightBackgroundLayer = current - } else { - highlightBackgroundLayer = SimpleLayer() - self.highlightBackgroundLayer = highlightBackgroundLayer - self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) - highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor - } - highlightBackgroundLayer.frame = highlightBackgroundFrame - highlightBackgroundLayer.opacity = 1.0 - } else { - if let highlightBackgroundLayer = self.highlightBackgroundLayer { - self.highlightBackgroundLayer = nil - highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in - highlightBackgroundLayer?.removeFromSuperlayer() - }) - } - } - } - self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func pressed() { - guard let component = self.component else { - return - } - component.action() - } - - func update(component: ActionListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme - - if self.component?.iconName != component.iconName { - if let iconName = component.iconName { - self.iconView.image = UIImage(bundleImageName: iconName)?.withRenderingMode(.alwaysTemplate) - } else { - self.iconView.image = nil - } - } - if themeUpdated { - self.iconView.tintColor = component.theme.list.itemAccentColor - } - - self.component = component - self.state = state - - let contextInset: CGFloat = 0.0 - - let height: CGFloat = 44.0 - let verticalInset: CGFloat = 1.0 - let leftInset: CGFloat = 62.0 + component.sideInset - let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset - - let previousTitleFrame = self.title.view?.frame - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) - ) - - let centralContentHeight: CGFloat = titleSize.height - - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.containerButton.addSubview(titleView) - } - titleView.frame = titleFrame - if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { - transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) - } - } - - if let iconImage = self.iconView.image { - transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - iconImage.size.width) / 2.0), y: floor((height - iconImage.size.height) / 2.0)), size: iconImage.size)) - } - - if themeUpdated { - self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor - } - transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) - self.separatorLayer.isHidden = !component.hasNext - - self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) - - let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) - transition.setFrame(view: self.containerButton, frame: containerFrame) - - return CGSize(width: availableSize.width, height: height) - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift new file mode 100644 index 0000000000..1788bc76f8 --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -0,0 +1,308 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode + +final class CategoryListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool, isTinted: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + let color: ShareWithPeersScreenComponent.CategoryColor + let iconName: String? + let subtitle: String? + let selectionState: SelectionState + let hasNext: Bool + let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + sideInset: CGFloat, + title: String, + color: ShareWithPeersScreenComponent.CategoryColor, + iconName: String?, + subtitle: String?, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.sideInset = sideInset + self.title = title + self.color = color + self.iconName = iconName + self.subtitle = subtitle + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let iconView: UIImageView + + private var checkLayer: CheckLayer? + + private var component: CategoryListItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + self.iconView = UIImageView() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + self.containerButton.addSubview(self.iconView) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + if (self.component?.iconName != component.iconName || self.component?.color != component.color), let iconName = component.iconName { + let mappedColor: AvatarBackgroundColor + var iconScale: CGFloat = 1.0 + switch component.color { + case .blue: + mappedColor = .blue + case .yellow: + mappedColor = .yellow + case .green: + mappedColor = .green + case .purple: + mappedColor = .purple + case .red: + mappedColor = .red + case .violet: + mappedColor = .violet + } + + iconScale = 1.0 + + self.iconView.image = generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white), iconScale: iconScale, cornerRadius: 20.0, color: mappedColor) + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 60.0 + let verticalInset: CGFloat = 1.0 + var leftInset: CGFloat = 62.0 + component.sideInset + let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + var avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected, isTinted) = component.selectionState { + leftInset += 44.0 + avatarLeftInset += 44.0 + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) + } + checkLayer.theme = theme + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + var theme = CheckNodeTheme(theme: component.theme, style: .plain) + if isTinted { + theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5) + } + checkLayer = CheckLayer(theme: theme) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let avatarSize: CGFloat = 40.0 + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + + if themeUpdated { + } + + transition.setFrame(view: self.iconView, frame: avatarFrame) + + let labelData: (String, Bool) = ("", false) + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated && !"".isEmpty { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSpacing: CGFloat = 1.0 + var centralContentHeight: CGFloat = titleSize.height + if !labelData.0.isEmpty { + centralContentHeight += titleSpacing + labelSize.height + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.containerButton.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift index 10f12a5634..14792c839d 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/PeerListItemComponent.swift @@ -11,6 +11,7 @@ import AvatarNode import TelegramPresentationData import CheckNode import TelegramStringFormatting +import PeerPresenceStatusManager private let avatarFont = avatarPlaceholderFont(size: 15.0) @@ -27,6 +28,7 @@ final class PeerListItemComponent: Component { let title: String let peer: EnginePeer? let subtitle: String? + let presence: EnginePeer.Presence? let selectionState: SelectionState let hasNext: Bool let action: (EnginePeer) -> Void @@ -39,6 +41,7 @@ final class PeerListItemComponent: Component { title: String, peer: EnginePeer?, subtitle: String?, + presence: EnginePeer.Presence?, selectionState: SelectionState, hasNext: Bool, action: @escaping (EnginePeer) -> Void @@ -50,6 +53,7 @@ final class PeerListItemComponent: Component { self.title = title self.peer = peer self.subtitle = subtitle + self.presence = presence self.selectionState = selectionState self.hasNext = hasNext self.action = action @@ -77,6 +81,9 @@ final class PeerListItemComponent: Component { if lhs.subtitle != rhs.subtitle { return false } + if lhs.presence != rhs.presence { + return false + } if lhs.selectionState != rhs.selectionState { return false } @@ -99,6 +106,8 @@ final class PeerListItemComponent: Component { private var component: PeerListItemComponent? private weak var state: EmptyComponentState? + private var presenceManager: PeerPresenceStatusManager? + override init(frame: CGRect) { self.separatorLayer = SimpleLayer() @@ -128,6 +137,12 @@ final class PeerListItemComponent: Component { } func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let animationHint = transition.userData(ShareWithPeersScreenComponent.AnimationHint.self) + var synchronousLoad = false + if let animationHint, animationHint.contentReloaded { + synchronousLoad = true + } + let themeUpdated = self.component?.theme !== component.theme var hasSelectionUpdated = false @@ -146,6 +161,23 @@ final class PeerListItemComponent: Component { } } + if let presence = component.presence { + let presenceManager: PeerPresenceStatusManager + if let current = self.presenceManager { + presenceManager = current + } else { + presenceManager = PeerPresenceStatusManager(update: { [weak self] in + self?.state?.updated(transition: .immediate) + }) + self.presenceManager = presenceManager + } + presenceManager.reset(presence: presence) + } else { + if self.presenceManager != nil { + self.presenceManager = nil + } + } + self.component = component self.state = state @@ -210,15 +242,15 @@ final class PeerListItemComponent: Component { } else { clipStyle = .round } - if peer.id == component.context.account.peerId { - self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - } else { - self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - } + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) } let labelData: (String, Bool) - if let subtitle = component.subtitle { + + if let presence = component.presence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(timestamp)) + } else if let subtitle = component.subtitle { labelData = (subtitle, false) } else if case .legacyGroup = component.peer { labelData = (component.strings.Group_Status, false) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift new file mode 100644 index 0000000000..5bb71c4073 --- /dev/null +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift @@ -0,0 +1,108 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent + +final class SectionHeaderComponent: Component { + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + + init( + theme: PresentationTheme, + sideInset: CGFloat, + title: String + ) { + self.theme = theme + self.sideInset = sideInset + self.title = title + } + + static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let backgroundView: BlurredBackgroundView + + private var component: SectionHeaderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let height: CGFloat = 28.0 + let leftInset: CGFloat = component.sideInset + let rightInset: CGFloat = component.sideInset + + let previousTitleFrame = self.title.view?.frame + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + let size = CGSize(width: availableSize.width, height: height) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 834c442be0..970642b91a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -15,37 +15,92 @@ import PresentationDataUtils import ButtonComponent import PlainButtonComponent import AnimatedCounterComponent +import TokenListTextField +import AvatarNode -/*private final class ShareWithPeersScreenComponent: Component { +final class ShareWithPeersScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let stateContext: ShareWithPeersScreen.StateContext + let categoryItems: [CategoryItem] + let completion: (EngineStoryPrivacy) -> Void init( - context: AccountContext + context: AccountContext, + stateContext: ShareWithPeersScreen.StateContext, + categoryItems: [CategoryItem], + completion: @escaping (EngineStoryPrivacy) -> Void ) { self.context = context + self.stateContext = stateContext + self.categoryItems = categoryItems + self.completion = completion } static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.stateContext !== rhs.stateContext { + return false + } + if lhs.categoryItems != rhs.categoryItems { + return false + } + return true } private struct ItemLayout: Equatable { + struct Section: Equatable { + var id: Int + var insets: UIEdgeInsets + var itemHeight: CGFloat + var itemCount: Int + + var totalHeight: CGFloat + + init( + id: Int, + insets: UIEdgeInsets, + itemHeight: CGFloat, + itemCount: Int + ) { + self.id = id + self.insets = insets + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.totalHeight = insets.top + itemHeight * CGFloat(itemCount) + } + } + var containerSize: CGSize var containerInset: CGFloat var bottomInset: CGFloat var topInset: CGFloat + var sideInset: CGFloat + var navigationHeight: CGFloat + var sections: [Section] + var contentHeight: CGFloat - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, contentHeight: CGFloat) { + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, navigationHeight: CGFloat, sections: [Section]) { self.containerSize = containerSize self.containerInset = containerInset self.bottomInset = bottomInset self.topInset = topInset + self.sideInset = sideInset + self.navigationHeight = navigationHeight + self.sections = sections + + var contentHeight: CGFloat = 0.0 + contentHeight += navigationHeight + for section in sections { + contentHeight += section.totalHeight + } + contentHeight += bottomInset self.contentHeight = contentHeight } } @@ -61,22 +116,113 @@ import AnimatedCounterComponent } final class AnimationHint { - init() { + let contentReloaded: Bool + + init( + contentReloaded: Bool + ) { + self.contentReloaded = contentReloaded } } + enum CategoryColor { + case blue + case yellow + case green + case purple + case red + case violet + } + + final class CategoryItem: Equatable { + let id: CategoryId + let title: String + let icon: String? + let iconColor: CategoryColor + let actionTitle: String? + + init( + id: CategoryId, + title: String, + icon: String?, + iconColor: CategoryColor, + actionTitle: String? + ) { + self.id = id + self.title = title + self.icon = icon + self.iconColor = iconColor + self.actionTitle = actionTitle + } + + static func ==(lhs: CategoryItem, rhs: CategoryItem) -> Bool { + if lhs === rhs { + return true + } + return false + } + } + + final class PeerItem: Equatable { + let id: EnginePeer.Id + let peer: EnginePeer? + + init( + id: EnginePeer.Id, + peer: EnginePeer? + ) { + self.id = id + self.peer = peer + } + + static func ==(lhs: PeerItem, rhs: PeerItem) -> Bool { + if lhs === rhs { + return true + } + return false + } + } + + enum CategoryId: Int, Hashable { + case everyone = 0 + case contacts = 1 + case closeFriends = 2 + case selectedContacts = 3 + } + final class View: UIView, UIScrollViewDelegate { private let dimView: UIView private let backgroundView: UIImageView + + private let navigationContainerView: UIView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationTitle = ComponentView() + private let navigationLeftButton = ComponentView() + private let navigationSeparatorLayer: SimpleLayer + private let navigationTextFieldState = TokenListTextField.ExternalState() + private let navigationTextField = ComponentView() + private let textFieldSeparatorLayer: SimpleLayer + private let scrollView: ScrollView private let scrollContentClippingView: SparseContainerView private let scrollContentView: UIView + private let bottomBackgroundView: BlurredBackgroundView + private let bottomSeparatorLayer: SimpleLayer + private let actionButton = ComponentView() + + private let categoryTemplateItem = ComponentView() + private let peerTemplateItem = ComponentView() + private let itemContainerView: UIView - private var items: [AnyHashable: ComponentView] = [:] + private var visibleSectionHeaders: [Int: ComponentView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] private var ignoreScrolling: Bool = false + private var selectedPeers: [EnginePeer.Id] = [] + private var selectedCategories = Set() + private var component: ShareWithPeersScreenComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? @@ -84,10 +230,30 @@ import AnimatedCounterComponent private var topOffsetDistance: CGFloat? + private var defaultStateValue: ShareWithPeersScreen.State? + private var stateDisposable: Disposable? + + private var searchStateContext: ShareWithPeersScreen.StateContext? + private var searchStateDisposable: Disposable? + + private var effectiveStateValue: ShareWithPeersScreen.State? { + return self.searchStateContext?.stateValue ?? self.defaultStateValue + } + + private var isDisplayingSearch: Bool = false + override init(frame: CGRect) { self.dimView = UIView() - self.backgroundView = UIImageView(image: generateStretchableFilledCircleImage(asdf)) + self.backgroundView = UIImageView() + + self.navigationContainerView = SparseContainerView() + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationSeparatorLayer = SimpleLayer() + self.textFieldSeparatorLayer = SimpleLayer() + + self.bottomBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.bottomSeparatorLayer = SimpleLayer() self.scrollView = ScrollView() @@ -100,17 +266,12 @@ import AnimatedCounterComponent self.itemContainerView.clipsToBounds = true self.itemContainerView.layer.cornerRadius = 10.0 - self.bottomBackgroundLayer = SimpleLayer() - self.bottomSeparatorLayer = SimpleLayer() - super.init(frame: frame) self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) + self.addSubview(self.backgroundView) - self.addSubview(self.navigationBarContainer) - - self.scrollView.delaysContentTouches = false + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -134,7 +295,11 @@ import AnimatedCounterComponent self.scrollContentView.addSubview(self.itemContainerView) - self.layer.addSublayer(self.bottomBackgroundLayer) + self.addSubview(self.navigationContainerView) + self.navigationContainerView.addSubview(self.navigationBackgroundView) + self.navigationContainerView.layer.addSublayer(self.navigationSeparatorLayer) + + self.addSubview(self.bottomBackgroundView) self.layer.addSublayer(self.bottomSeparatorLayer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) @@ -145,7 +310,7 @@ import AnimatedCounterComponent } deinit { - self.joinDisposable?.dispose() + self.stateDisposable?.dispose() } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -160,15 +325,14 @@ import AnimatedCounterComponent } if scrollView.contentOffset.y <= -100.0 && velocity.y <= -2.0 { - self.environment?.controller()?.dismiss() } else { var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset if topOffset > 0.0 { topOffset = max(0.0, topOffset) if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + //targetContentOffset.pointee.y = scrollView.contentOffset.y + //scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) } } } @@ -178,11 +342,11 @@ import AnimatedCounterComponent if !self.bounds.contains(point) { return nil } - if !self.backgroundLayer.frame.contains(point) { + if !self.backgroundView.frame.contains(point) { return self.dimView } - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + if let result = self.navigationContainerView.hitTest(self.convert(point, to: self.navigationContainerView), with: event) { return result } @@ -200,40 +364,278 @@ import AnimatedCounterComponent } private func updateScrolling(transition: Transition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + guard let component = self.component, let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { return } + guard let stateValue = self.effectiveStateValue else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + transition.setTransform(layer: self.backgroundView.layer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + transition.setPosition(view: self.navigationContainerView, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY let bottomAlphaDistance: CGFloat = 30.0 var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance bottomAlpha = max(0.0, min(1.0, bottomAlpha)) - let bottomOverlayAlpha: CGFloat = bottomAlpha - transition.setAlpha(layer: self.bottomBackgroundLayer, alpha: bottomOverlayAlpha) - transition.setAlpha(layer: self.bottomSeparatorLayer, alpha: bottomOverlayAlpha) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) self.topOffsetDistance = topOffsetDistance var topOffsetFraction = topOffset / topOffsetDistance topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) let transitionFactor: CGFloat = 1.0 - topOffsetFraction - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + let _ = transitionFactor + let _ = controller + //controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + + var visibleBounds = self.scrollView.bounds + visibleBounds.origin.y -= itemLayout.topInset + visibleBounds.size.height += itemLayout.topInset + + var visibleFrame = self.scrollView.frame + visibleFrame.origin.y -= itemLayout.topInset + visibleFrame.size.height += itemLayout.topInset + + var validIds: [AnyHashable] = [] + var validSectionHeaders: [AnyHashable] = [] + var sectionOffset: CGFloat = itemLayout.navigationHeight + for sectionIndex in 0 ..< itemLayout.sections.count { + let section = itemLayout.sections[sectionIndex] + + var minSectionHeader: UIView? + + do { + var sectionHeaderFrame = CGRect(origin: CGPoint(x: 0.0, y: itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) + + let sectionHeaderMinY = topOffset + itemLayout.containerInset + itemLayout.navigationHeight + let sectionHeaderMaxY = itemLayout.containerInset + sectionOffset - self.scrollView.bounds.minY + itemLayout.topInset + section.totalHeight - 28.0 + + sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) + sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + + if visibleFrame.intersects(sectionHeaderFrame) { + validSectionHeaders.append(section.id) + let sectionHeader: ComponentView + var sectionHeaderTransition = transition + if let current = self.visibleSectionHeaders[section.id] { + sectionHeader = current + } else { + if !transition.animation.isImmediate { + sectionHeaderTransition = .immediate + } + sectionHeader = ComponentView() + self.visibleSectionHeaders[section.id] = sectionHeader + } + + let sectionTitle: String + if section.id == 0 { + sectionTitle = "WHO CAN VIEW FOR 24 HOURS" + } else { + sectionTitle = "CONTACTS" + } + + let _ = sectionHeader.update( + transition: sectionHeaderTransition, + component: AnyComponent(SectionHeaderComponent( + theme: environment.theme, + sideInset: 16.0, + title: sectionTitle + )), + environment: {}, + containerSize: sectionHeaderFrame.size + ) + if let sectionHeaderView = sectionHeader.view { + if sectionHeaderView.superview == nil { + sectionHeaderView.isUserInteractionEnabled = false + self.scrollContentClippingView.addSubview(sectionHeaderView) + } + if minSectionHeader == nil { + minSectionHeader = sectionHeaderView + } + sectionHeaderTransition.setFrame(view: sectionHeaderView, frame: sectionHeaderFrame) + } + } + } + + if section.id == 0 { + for i in 0 ..< component.categoryItems.count { + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let item = component.categoryItems[i] + let categoryId = item.id + let itemId = AnyHashable(item.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(CategoryListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: itemLayout.sideInset, + title: item.title, + color: item.iconColor, + iconName: item.icon, + subtitle: item.actionTitle, + selectionState: .editing(isSelected: self.selectedCategories.contains(item.id), isTinted: false), + hasNext: i != component.categoryItems.count - 1, + action: { [weak self] in + guard let self else { + return + } + switch categoryId { + case .everyone: + if self.selectedCategories.contains(categoryId) { + } else { + self.selectedCategories.removeAll() + self.selectedCategories.insert(categoryId) + } + case .contacts, .closeFriends, .selectedContacts: + if self.selectedCategories.contains(categoryId) { + } else { + self.selectedCategories.removeAll() + self.selectedCategories.insert(categoryId) + } + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + if let minSectionHeader { + self.itemContainerView.insertSubview(itemView, belowSubview: minSectionHeader) + } else { + self.itemContainerView.addSubview(itemView) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + } else if section.id == 1 { + for i in 0 ..< stateValue.peers.count { + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + let peer = stateValue.peers[i] + let itemId = AnyHashable(peer.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: itemLayout.sideInset, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + subtitle: nil, + presence: stateValue.presences[peer.id], + selectionState: .editing(isSelected: self.selectedPeers.contains(peer.id), isTinted: false), + hasNext: true, + action: { [weak self] peer in + guard let self else { + return + } + if let index = self.selectedPeers.firstIndex(of: peer.id) { + self.selectedPeers.remove(at: index) + } else { + self.selectedPeers.append(peer.id) + } + + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + + if self.searchStateContext != nil { + if let navigationTextFieldView = self.navigationTextField.view as? TokenListTextField.View { + navigationTextFieldView.clearText() + } + } + } + )), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + } + + sectionOffset += section.totalHeight + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + itemView.removeFromSuperview() + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + var removeSectionHeaderIds: [Int] = [] + for (id, item) in self.visibleSectionHeaders { + if !validSectionHeaders.contains(id) { + removeSectionHeaderIds.append(id) + if let itemView = item.view { + itemView.removeFromSuperview() + } + } + } + for id in removeSectionHeaderIds { + self.visibleSectionHeaders.removeValue(forKey: id) + } } func animateIn() { self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + let animateOffset: CGFloat = self.bounds.height - self.backgroundView.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomBackgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationContainerView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomBackgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.bottomSeparatorLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -245,7 +647,7 @@ import AnimatedCounterComponent controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) } - var animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + var animateOffset: CGFloat = self.bounds.height - self.backgroundView.frame.minY if self.scrollView.contentOffset.y < 0.0 { animateOffset += -self.scrollView.contentOffset.y } @@ -254,10 +656,10 @@ import AnimatedCounterComponent self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomBackgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.backgroundView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationContainerView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomBackgroundView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.bottomSeparatorLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) if let actionButtonView = self.actionButton.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } @@ -267,7 +669,7 @@ import AnimatedCounterComponent let animationHint = transition.userData(AnimationHint.self) var contentTransition = transition - if animationHint != nil { + if let animationHint, animationHint.contentReloaded, !transition.animation.isImmediate { contentTransition = .immediate } @@ -276,24 +678,24 @@ import AnimatedCounterComponent let resetScrolling = self.scrollView.bounds.width != availableSize.width - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 0.0 - if self.component?.linkContents == nil, let linkContents = component.linkContents { - if case let .remove(_, defaultSelectedPeerIds) = component.subject { - for peer in linkContents.peers { - if defaultSelectedPeerIds.contains(peer.id) { - self.selectedItems.insert(peer.id) - } + if self.component == nil { + self.selectedCategories.insert(.everyone) + + var applyState = false + self.defaultStateValue = component.stateContext.stateValue + self.stateDisposable = (component.stateContext.state + |> deliverOnMainQueue).start(next: { [weak self] stateValue in + guard let self else { + return } - } else { - for peer in linkContents.peers { - self.selectedItems.insert(peer.id) + self.defaultStateValue = stateValue + if applyState { + self.state?.updated(transition: .immediate) } - } - } - - if self.component == nil, case let .linkList(_, initialLinks) = component.subject { - self.linkListItems = initialLinks + }) + applyState = true } self.component = component @@ -302,600 +704,244 @@ import AnimatedCounterComponent if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor - self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - self.bottomBackgroundLayer.backgroundColor = environment.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + self.scrollView.indicatorStyle = environment.theme.overallDarkAppearance ? .white : .black + + self.backgroundView.image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(environment.theme.list.plainBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height * 0.5), size: CGSize(width: size.width, height: size.height * 0.5))) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 19) + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + self.textFieldSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + self.bottomBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } + var tokens: [TokenListTextField.Token] = [] + for categoryId in self.selectedCategories.sorted(by: { $0.rawValue < $1.rawValue }) { + let categoryTitle: String + var categoryImage: UIImage? + switch categoryId { + case .everyone: + categoryTitle = "Everyone" + categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue) + case .contacts: + categoryTitle = "Contacts" + categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.9, cornerRadius: 6.0, circleCorners: true, color: .yellow) + case .closeFriends: + categoryTitle = "Close Friends" + categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .green) + case .selectedContacts: + categoryTitle = "Selected Contacts" + categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .purple) + } + tokens.append(TokenListTextField.Token( + id: AnyHashable(categoryId), + title: categoryTitle, + fixedPosition: categoryId.rawValue, + content: .category(categoryImage) + )) + } + for peerId in self.selectedPeers { + guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { + continue + } + tokens.append(TokenListTextField.Token( + id: AnyHashable(peerId), + title: peer.compactDisplayTitle, + fixedPosition: nil, + content: .peer(peer) + )) + } + + self.navigationTextField.parentState = state + let navigationTextFieldSize = self.navigationTextField.update( + transition: transition, + component: AnyComponent(TokenListTextField( + externalState: self.navigationTextFieldState, + context: component.context, + theme: environment.theme, + placeholder: "Search Contacts", + tokens: tokens, + sideInset: sideInset, + deleteToken: { [weak self] tokenId in + guard let self else { + return + } + if let categoryId = tokenId.base as? CategoryId { + self.selectedCategories.remove(categoryId) + } else if let peerId = tokenId.base as? EnginePeer.Id { + self.selectedPeers.removeAll(where: { $0 == peerId }) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + if !self.navigationTextFieldState.text.isEmpty { + if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { + } else { + self.searchStateDisposable?.dispose() + let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) + var applyState = false + self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.searchStateContext = searchStateContext + if applyState { + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + } + }) + applyState = true + } + } else if let _ = self.searchStateContext { + self.searchStateContext = nil + self.searchStateDisposable?.dispose() + self.searchStateDisposable = nil + + contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + } + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - var contentHeight: CGFloat = 0.0 + let categoryItemSize = self.categoryTemplateItem.update( + transition: .immediate, + component: AnyComponent(CategoryListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: sideInset, + title: "Title", + color: .blue, + iconName: nil, + subtitle: nil, + selectionState: .editing(isSelected: false, isTinted: false), + hasNext: true, + action: {} + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let peerItemSize = self.peerTemplateItem.update( + transition: transition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: sideInset, + title: "Name", + peer: nil, + subtitle: nil, + presence: nil, + selectionState: .editing(isSelected: false, isTinted: false), + hasNext: true, + action: { _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) - let leftButtonSize = self.leftButton.update( - transition: contentTransition, + var sections: [ItemLayout.Section] = [] + if let stateValue = self.effectiveStateValue { + if self.searchStateContext == nil { + sections.append(ItemLayout.Section( + id: 0, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), + itemHeight: categoryItemSize.height, + itemCount: component.categoryItems.count + )) + } + sections.append(ItemLayout.Section( + id: 1, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), + itemHeight: peerItemSize.height, + itemCount: stateValue.peers.count + )) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var navigationHeight: CGFloat = 56.0 + + let navigationSideInset: CGFloat = 16.0 + let navigationLeftButtonSize = self.navigationLeftButton.update( + transition: transition, component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), + content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { + guard let self, let environment = self.environment, let controller = environment.controller() else { return } controller.dismiss() } - ).minSize(CGSize(width: 44.0, height: 56.0))), + ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) + containerSize: CGSize(width: availableSize.width, height: navigationHeight) ) - let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) + if let navigationLeftButtonView = self.navigationLeftButton.view { + if navigationLeftButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationLeftButtonView) } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) } - let titleString: String - var allChatsAdded = false - var canAddChatCount = 0 - if case .linkList = component.subject { - titleString = environment.strings.FolderLinkPreview_TitleShare - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - titleString = environment.strings.FolderLinkPreview_TitleRemove - } else if linkContents.localFilterId != nil { - if linkContents.alreadyMemberPeerIds == Set(linkContents.peers.map(\.id)) { - allChatsAdded = true - } - canAddChatCount = linkContents.peers.map(\.id).count - linkContents.alreadyMemberPeerIds.count - - if allChatsAdded { - titleString = environment.strings.FolderLinkPreview_TitleAddFolder - } else { - titleString = environment.strings.FolderLinkPreview_TitleAddChats(Int32(canAddChatCount)) - } - } else { - titleString = environment.strings.FolderLinkPreview_TitleAddFolder - } - } else { - titleString = " " - } - let titleSize = self.title.update( + let navigationTitleSize = self.navigationTitle.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), + component: AnyComponent(Text(text: "Share Story", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - navigationSideInset - navigationLeftButtonFrame.maxX, height: navigationHeight) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 18.0), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + self.navigationContainerView.addSubview(navigationTitleView) } - contentTransition.setFrame(view: titleView, frame: titleFrame) + transition.setPosition(view: navigationTitleView, position: navigationTitleFrame.center) + navigationTitleView.bounds = CGRect(origin: CGPoint(), size: navigationTitleFrame.size) } - contentHeight += 44.0 - contentHeight += 14.0 - - var topBadge: String? - if case .linkList = component.subject { - } else if case .remove = component.subject { - } else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil, canAddChatCount != 0 { - topBadge = "+\(canAddChatCount)" - } - - let topIconSize = self.topIcon.update( - transition: contentTransition, - component: AnyComponent(ChatFolderLinkHeaderComponent( - theme: environment.theme, - strings: environment.strings, - title: component.linkContents?.title ?? "Folder", - badge: topBadge - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) - ) - let topIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topIconSize.width) * 0.5), y: contentHeight), size: topIconSize) - if let topIconView = self.topIcon.view { - if topIconView.superview == nil { - self.scrollContentView.addSubview(topIconView) + let navigationTextFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: navigationTextFieldSize) + if let navigationTextFieldView = self.navigationTextField.view { + if navigationTextFieldView.superview == nil { + self.navigationContainerView.addSubview(navigationTextFieldView) + self.navigationContainerView.layer.addSublayer(self.textFieldSeparatorLayer) } - contentTransition.setFrame(view: topIconView, frame: topIconFrame) - topIconView.isHidden = component.linkContents == nil + transition.setFrame(view: navigationTextFieldView, frame: navigationTextFieldFrame) + transition.setFrame(layer: self.textFieldSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationTextFieldFrame.maxY), size: CGSize(width: navigationTextFieldFrame.width, height: UIScreenPixel))) } + navigationHeight += navigationTextFieldFrame.height - contentHeight += topIconSize.height - contentHeight += 20.0 - - let text: String - if case .linkList = component.subject { - text = environment.strings.FolderLinkPreview_TextLinkList - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - text = environment.strings.FolderLinkPreview_TextRemoveFolder - } else if allChatsAdded { - text = environment.strings.FolderLinkPreview_TextAllAdded - } else if linkContents.localFilterId == nil { - text = environment.strings.FolderLinkPreview_TextAddFolder - } else { - let chatCountString: String = environment.strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount)) - text = environment.strings.FolderLinkPreview_TextAddChats(chatCountString, linkContents.title ?? "").string - } + let topInset: CGFloat + if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { + topInset = 0.0 } else { - text = " " + topInset = max(0.0, availableSize.height - containerInset - 600.0) } - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor) + self.navigationBackgroundView.update(size: CGSize(width: availableSize.width, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: navigationHeight))) - let descriptionTextSize = self.descriptionText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: text, attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - )), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) - ) - let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) - if let descriptionTextView = self.descriptionText.view { - if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) - } - descriptionTextView.bounds = CGRect(origin: CGPoint(), size: descriptionTextFrame.size) - contentTransition.setPosition(view: descriptionTextView, position: descriptionTextFrame.center) - } + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - contentHeight += descriptionTextFrame.height - contentHeight += 39.0 - - var singleItemHeight: CGFloat = 0.0 - - var itemsHeight: CGFloat = 0.0 - var validIds: [AnyHashable] = [] - if case let .linkList(folderId, _) = component.subject { - do { - let id = AnyHashable("action") - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(ActionListItemComponent( - theme: environment.theme, - sideInset: 0.0, - iconName: "Contact List/LinkActionIcon", - title: environment.strings.InviteLink_Create, - hasNext: !self.linkListItems.isEmpty, - action: { [weak self] in - self?.openCreateLink() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height - } - - for i in 0 ..< self.linkListItems.count { - let link = self.linkListItems[i] - - let id = AnyHashable(link.link) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - let subtitle: String = environment.strings.ChatListFilter_LinkLabelChatCount(Int32(link.peerIds.count)) - - let itemComponent = LinkListItemComponent( - theme: environment.theme, - sideInset: 0.0, - title: link.title.isEmpty ? link.link : link.title, - link: link, - label: subtitle, - selectionState: .none, - hasNext: i != self.linkListItems.count - 1, - action: { [weak self] link in - guard let self else { - return - } - self.openLink(link: link) - }, - contextAction: { [weak self] link, sourceView, gesture in - guard let self, let component = self.component, let environment = self.environment else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - var itemList: [ContextMenuItem] = [] - - itemList.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(.default) - - UIPasteboard.general.string = link.link - - if let self, let component = self.component, let controller = self.environment?.controller() { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - }))) - - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - if let self, let component = self.component, let controller = self.environment?.controller() { - controller.present(QrCodeScreen(context: component.context, updatedPresentationData: nil, subject: .chatFolder(slug: link.slug)), in: .window(.root)) - } - }))) - - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - if let self, let component = self.component { - self.linkListItems.removeAll(where: { $0.link == link.link }) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - - let context = component.context - let _ = (context.engine.peers.editChatFolderLink(filterId: folderId, link: link, title: nil, peerIds: nil, revoke: true) - |> deliverOnMainQueue).start(completed: { - let _ = (context.engine.peers.deleteChatFolderLink(filterId: folderId, link: link) - |> deliverOnMainQueue).start(completed: { - }) - }) - } - }))) - - let items = ContextController.Items(content: .list(itemList)) - - let controller = ContextController( - account: component.context.account, - presentationData: presentationData, - source: .extracted(LinkListContextExtractedContentSource(contentView: sourceView)), - items: .single(items), - recognizer: nil, - gesture: gesture - ) - - environment.controller()?.forEachController({ controller in - if let controller = controller as? UndoOverlayController { - controller.dismiss() - } - return true - }) - environment.controller()?.presentInGlobalOverlay(controller) - } - ) - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(itemComponent), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height - } - } else if let linkContents = component.linkContents { - for i in 0 ..< linkContents.peers.count { - let peer = linkContents.peers[i] - - let id = AnyHashable(peer.id) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - var subtitle: String? - if case let .channel(channel) = peer, case .broadcast = channel.info { - if linkContents.alreadyMemberPeerIds.contains(peer.id) { - subtitle = environment.strings.FolderLinkPreview_LabelPeerSubscriber - } else if let memberCount = linkContents.memberCounts[peer.id] { - subtitle = environment.strings.FolderLinkPreview_LabelPeerSubscribers(Int32(memberCount)) - } - } else { - if linkContents.alreadyMemberPeerIds.contains(peer.id) { - subtitle = environment.strings.FolderLinkPreview_LabelPeerMember - } else if let memberCount = linkContents.memberCounts[peer.id] { - subtitle = environment.strings.FolderLinkPreview_LabelPeerMembers(Int32(memberCount)) - } - } - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - subtitle: subtitle, - selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)), - hasNext: i != linkContents.peers.count - 1, - action: { [weak self] peer in - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { - return - } - - if case .remove = component.subject { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } else if linkContents.alreadyMemberPeerIds.contains(peer.id) { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let text: String - if case let .channel(channel) = peer, case .broadcast = channel.info { - text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberChannel - } else { - text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberGroup - } - controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height - } - } - - var removeIds: [AnyHashable] = [] - for (id, item) in self.items { - if !validIds.contains(id) { - removeIds.append(id) - item.view?.removeFromSuperview() - } - } - for id in removeIds { - self.items.removeValue(forKey: id) - } - - let listHeaderTitle: String - if case .linkList = component.subject { - listHeaderTitle = environment.strings.FolderLinkPreview_LinkSectionHeader - } else if let linkContents = component.linkContents { - if case .remove = component.subject { - listHeaderTitle = environment.strings.FolderLinkPreview_RemoveSectionSelectedHeader(Int32(linkContents.peers.count)) - } else if allChatsAdded { - listHeaderTitle = environment.strings.FolderLinkPreview_ChatSectionHeader(Int32(linkContents.peers.count)) - } else { - listHeaderTitle = environment.strings.FolderLinkPreview_ChatSectionJoinHeader(Int32(linkContents.peers.count)) - } - } else { - listHeaderTitle = " " - } - - var listHeaderActionItems: [AnimatedCounterComponent.Item] = [] - - let dynamicIndex = environment.strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{dynamic}") - let staticIndex = environment.strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{static}") - var headerActionItemIndices: [Int: Int] = [:] - if let dynamicIndex, let staticIndex { - if dynamicIndex.lowerBound < staticIndex.lowerBound { - headerActionItemIndices[0] = 0 - headerActionItemIndices[1] = 1 - } else { - headerActionItemIndices[0] = 1 - headerActionItemIndices[1] = 0 - } - } else if dynamicIndex != nil { - headerActionItemIndices[0] = 0 - } else if staticIndex != nil { - headerActionItemIndices[1] = 0 - } - - let dynamicItem: AnimatedCounterComponent.Item - let staticItem: AnimatedCounterComponent.Item - - if self.selectedItems.count == self.items.count { - dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartDeselect, numericValue: 0) - staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllStaticPartDeselect, numericValue: 1) - } else { - dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartSelect, numericValue: 1) - staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: environment.strings.FolderLinkPreview_ListSelectionSelectAllStaticPartSelect, numericValue: 1) - } - - if let dynamicIndex = headerActionItemIndices[0], let staticIndex = headerActionItemIndices[1] { - if dynamicIndex < staticIndex { - listHeaderActionItems = [dynamicItem, staticItem] - } else { - listHeaderActionItems = [staticItem, dynamicItem] - } - } else if headerActionItemIndices[0] != nil { - listHeaderActionItems = [dynamicItem] - } else if headerActionItemIndices[1] != nil { - listHeaderActionItems = [staticItem] - } - - let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: environment.theme.list.freeTextColor) - - let listHeaderTextSize = self.listHeaderText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown( - text: listHeaderTitle, - attributes: MarkdownAttributes( - body: listHeaderBody, - bold: listHeaderBody, - link: listHeaderBody, - linkAttribute: { _ in nil } - ) - ) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0) - ) - if let listHeaderTextView = self.listHeaderText.view { - if listHeaderTextView.superview == nil { - listHeaderTextView.layer.anchorPoint = CGPoint() - self.scrollContentView.addSubview(listHeaderTextView) - } - let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize) - contentTransition.setPosition(view: listHeaderTextView, position: listHeaderTextFrame.origin) - listHeaderTextView.bounds = CGRect(origin: CGPoint(), size: listHeaderTextFrame.size) - listHeaderTextView.isHidden = component.linkContents == nil - } - - let listHeaderActionSize = self.listHeaderAction.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(AnimatedCounterComponent( - font: Font.regular(13.0), - color: environment.theme.list.itemAccentColor, - alignment: .right, - items: listHeaderActionItems - )), - effectAlignment: .right, - action: { [weak self] in - guard let self, let component = self.component, let linkContents = component.linkContents else { - return - } - if self.selectedItems.count != linkContents.peers.count { - for peer in linkContents.peers { - self.selectedItems.insert(peer.id) - } - } else { - self.selectedItems.removeAll() - for peerId in linkContents.alreadyMemberPeerIds { - self.selectedItems.insert(peerId) - } - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0) - ) - if let listHeaderActionView = self.listHeaderAction.view { - if listHeaderActionView.superview == nil { - listHeaderActionView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - self.scrollContentView.addSubview(listHeaderActionView) - } - let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize) - contentTransition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame) - - if let linkContents = component.linkContents, !allChatsAdded, linkContents.peers.count > 1 { - listHeaderActionView.isHidden = false - } else { - listHeaderActionView.isHidden = true - } - } - - contentHeight += listHeaderTextSize.height - contentHeight += 6.0 - - contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) - - var initialContentHeight = contentHeight - initialContentHeight += min(itemsHeight, floor(singleItemHeight * 3.5)) - - contentHeight += itemsHeight - contentHeight += 24.0 - initialContentHeight += 24.0 - - let actionButtonTitle: String - var actionButtonBadge: Int = 0 - if case .remove = component.subject { - actionButtonBadge = self.selectedItems.count - if self.selectedItems.isEmpty { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonRemoveFolder - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonRemoveFolderAndChats - } - } else if allChatsAdded { - actionButtonBadge = 0 - actionButtonTitle = environment.strings.Common_OK - } else if let linkContents = component.linkContents { - actionButtonBadge = max(0, self.selectedItems.count - (linkContents.peers.count - canAddChatCount)) - if linkContents.localFilterId != nil { - if actionButtonBadge == 0 { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonDoNotJoinChats - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonJoinChats - } - } else { - actionButtonTitle = environment.strings.FolderLinkPreview_ButtonAddFolder - } - } else { - actionButtonTitle = " " + var actionButtonTitle: String = "Post Story" + if self.selectedCategories.contains(.everyone) { + actionButtonTitle = "Post Story" + } else if self.selectedCategories.contains(.closeFriends) { + actionButtonTitle = "Send to Close Friends" + } else if self.selectedCategories.contains(.contacts) { + actionButtonTitle = "Send to Contacts" + } else if self.selectedCategories.contains(.selectedContacts) { + actionButtonTitle = "Send to Selected Contacts" } let actionButtonSize = self.actionButton.update( @@ -910,292 +956,62 @@ import AnimatedCounterComponent id: actionButtonTitle, component: AnyComponent(ButtonTextContentComponent( text: actionButtonTitle, - badge: actionButtonBadge, + badge: 0, textColor: environment.theme.list.itemCheckColors.foregroundColor, badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, badgeForeground: environment.theme.list.itemCheckColors.fillColor )) ), - isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil, - displaysProgress: self.inProgress, + isEnabled: true, + displaysProgress: false, action: { [weak self] in - guard let self, let component = self.component, let linkContents = component.linkContents, let controller = self.environment?.controller() else { + guard let self, let component = self.component, let controller = self.environment?.controller() else { return } - if case let .remove(folderId, _) = component.subject { - self.inProgress = true - self.state?.updated(transition: .immediate) - - component.completion?() - - let disposable = DisposableSet() - disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(self.selectedItems))) - disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId])) - - let folderTitle = linkContents.title ?? "" - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - - var additionalText: String? - if !self.selectedItems.isEmpty { - additionalText = presentationData.strings.FolderLinkPreview_ToastLeftChatsText(Int32(self.selectedItems.count)) - } - - var chatListController: ChatListController? - if let navigationController = controller.navigationController as? NavigationController { - for viewController in navigationController.viewControllers.reversed() { - if viewController is ShareWithPeersScreen { - continue - } - - if let rootController = viewController as? TabBarController { - for c in rootController.controllers { - if let c = c as? ChatListController { - chatListController = c - break - } - } - } else if let c = viewController as? ChatListController { - chatListController = c - break - } - - break - } - } - - let context = component.context - let selectedItems = self.selectedItems - let undoOverlayController = UndoOverlayController( - presentationData: presentationData, - content: .removedChat(title: presentationData.strings.FolderLinkPreview_ToastLeftTitle(folderTitle).string, text: additionalText), - elevatedLayout: false, - action: { value in - if case .commit = value { - let _ = (context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(selectedItems)) - |> deliverOnMainQueue).start(completed: { - Queue.mainQueue().after(1.0, { - disposable.dispose() - }) - }) - return true - } else if case .undo = value { - disposable.dispose() - return true - } - return false - } - ) - - if let chatListController, chatListController.view.window != nil { - chatListController.present(undoOverlayController, in: .current) - } else { - controller.present(undoOverlayController, in: .window(.root)) - } - - controller.dismiss() - } else if allChatsAdded { - controller.dismiss() - } else if let _ = component.linkContents { - if self.joinDisposable == nil, !self.selectedItems.isEmpty { - let joinSignal: Signal - switch component.subject { - case .linkList, .remove: - return - case let .slug(slug): - joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems)) - |> map(Optional.init) - case let .updates(updates): - var result: JoinChatFolderResult? - if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title { - result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: self.selectedItems.count) - } - joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems)) - |> map { _ -> JoinChatFolderResult? in - } - |> then(Signal.single(result)) - } - - self.inProgress = true - self.state?.updated(transition: .immediate) - - self.joinDisposable = (joinSignal - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - if let result, let navigationController = controller.navigationController as? NavigationController { - var chatListController: ChatListController? - for viewController in navigationController.viewControllers { - if let rootController = viewController as? TabBarController { - for c in rootController.controllers { - if let c = c as? ChatListController { - chatListController = c - break - } - } - } else if let c = viewController as? ChatListController { - chatListController = c - break - } - } - - if let chatListController { - navigationController.popToRoot(animated: true) - let context = component.context - chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in - guard let context, let chatListController else { - return - } - - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - - var isUpdates = false - if case .updates = component.subject { - isUpdates = true - } else { - if component.linkContents?.localFilterId != nil { - isUpdates = true - } - } - - if isUpdates { - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } else if result.newChatCount != 0 { - let animationBackgroundColor: UIColor - if presentationData.theme.overallDarkAppearance { - animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor - } else { - animationBackgroundColor = UIColor(rgb: 0x474747) - } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount)), customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } else { - let animationBackgroundColor: UIColor - if presentationData.theme.overallDarkAppearance { - animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor - } else { - animationBackgroundColor = UIColor(rgb: 0x474747) - } - chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitle(result.title).string, text: "", customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current) - } - }) - } - } - - controller.dismiss() - }, error: { [weak self] error in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let context = component.context - let navigationController = controller.navigationController as? NavigationController - - switch error { - case .generic: - controller.dismiss() - case let .dialogFilterLimitExceeded(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .folders, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: context, source: .folders)) - }) - controller.push(limitController) - controller.dismiss() - case let .sharedFolderLimitExceeded(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders)) - }) - controller.push(limitController) - controller.dismiss() - case let .tooManyChannels(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) - }) - controller.push(limitController) - controller.dismiss() - case let .tooManyChannelsInAccount(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) - }) - controller.push(limitController) - controller.dismiss() - } - }) - } else { - controller.dismiss() - } - } + component.completion(EngineStoryPrivacy( + base: .everyone, + additionallyIncludePeers: self.selectedPeers + )) + controller.dismiss() } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 50.0) ) var bottomPanelHeight: CGFloat = 0.0 - - if case .linkList = component.subject { - bottomPanelHeight += 30.0 + if environment.inputHeight != 0.0 { + bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height } else { - bottomPanelHeight += 14.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height + } + let actionButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) } - - transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight))) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } - if let controller = environment.controller() { - let subLayout = ContainerViewLayout( - size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset), - safeInsets: UIEdgeInsets(), - additionalInsets: UIEdgeInsets(), - statusBarHeight: nil, - inputHeight: nil, - inputHeightIsInteractivellyChanging: false, - inVoiceOver: false - ) - controller.presentationContext.containerLayoutUpdated(subLayout, transition: transition.containedViewLayoutTransition) - } + transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0))) + self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - contentHeight += bottomPanelHeight - initialContentHeight += bottomPanelHeight + let itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: bottomPanelHeight + environment.safeInsets.bottom, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) + let previousItemLayout = self.itemLayout + self.itemLayout = itemLayout - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: availableSize.width, height: itemLayout.contentHeight))) - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset, availableSize.height - containerInset) - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight) + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: itemLayout.contentHeight))) - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + transition.setPosition(view: self.backgroundView, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: availableSize)) - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame: CGRect - if case .linkList = component.subject { - scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - (containerInset + 56.0) + 1000.0)) - } else { - scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - bottomPanelHeight - 8.0 - (containerInset + 56.0))) - } + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 10.0), size: CGSize(width: availableSize.width, height: availableSize.height - 10.0)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) @@ -1205,211 +1021,26 @@ import AnimatedCounterComponent if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize } + let indicatorInsets = UIEdgeInsets(top: max(itemLayout.containerInset, environment.safeInsets.top + navigationHeight), left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) + if indicatorInsets != self.scrollView.scrollIndicatorInsets { + self.scrollView.scrollIndicatorInsets = indicatorInsets + } if resetScrolling { self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } else if let previousItemLayout, previousItemLayout.topInset != topInset { + let topInsetDifference = previousItemLayout.topInset - topInset + var scrollBounds = self.scrollView.bounds + scrollBounds.origin.y += -topInsetDifference + scrollBounds.origin.y = max(0.0, min(scrollBounds.origin.y, self.scrollView.contentSize.height - scrollBounds.height)) + let visibleDifference = self.scrollView.bounds.origin.y - scrollBounds.origin.y + self.scrollView.bounds = scrollBounds + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: visibleDifference), to: CGPoint(), additive: true) } self.ignoreScrolling = false - self.updateScrolling(transition: transition) + self.updateScrolling(transition: contentTransition) return availableSize } - - private func openLink(link: ExportedChatFolderLink) { - guard let component = self.component else { - return - } - guard case let .linkList(folderId, _) = component.subject else { - return - } - - let _ = (component.context.engine.peers.currentChatListFilters() - |> deliverOnMainQueue).start(next: { [weak self] filters in - guard let self, let component = self.component else { - return - } - guard let filter = filters.first(where: { $0.id == folderId }) else { - return - } - guard case let .filter(_, title, _, data) = filter else { - return - } - - let peerIds = data.includePeers.peers - let _ = (component.context.engine.data.get( - EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) - ) - |> deliverOnMainQueue).start(next: { [weak self] peers in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let peers = peers.compactMap({ peer -> EnginePeer? in - guard let peer else { - return nil - } - if case let .legacyGroup(group) = peer, group.migrationReference != nil { - return nil - } - return peer - }) - - let navigationController = controller.navigationController - controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { _ in }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) - })) - controller.dismiss() - }) - }) - } - - private func openCreateLink() { - guard let component = self.component else { - return - } - guard case let .linkList(folderId, _) = component.subject else { - return - } - - let _ = (component.context.engine.peers.currentChatListFilters() - |> deliverOnMainQueue).start(next: { [weak self] filters in - guard let self, let component = self.component else { - return - } - guard let filter = filters.first(where: { $0.id == folderId }) else { - return - } - guard case let .filter(_, title, _, data) = filter else { - return - } - - let peerIds = data.includePeers.peers - let _ = (component.context.engine.data.get( - EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) - ) - |> deliverOnMainQueue).start(next: { [weak self] peers in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let peers = peers.compactMap({ peer -> EnginePeer? in - guard let peer else { - return nil - } - if case let .legacyGroup(group) = peer, group.migrationReference != nil { - return nil - } - return peer - }) - if peers.allSatisfy({ !canShareLinkToPeer(peer: $0) }) { - let navigationController = controller.navigationController - controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: nil, linkUpdated: { _ in }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) - })) - } else { - var enabledPeerIds: [EnginePeer.Id] = [] - for peer in peers { - if canShareLinkToPeer(peer: peer) { - enabledPeerIds.append(peer.id) - } - } - - let _ = (component.context.engine.peers.exportChatFolder(filterId: folderId, title: "", peerIds: enabledPeerIds) - |> deliverOnMainQueue).start(next: { [weak self] link in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - self.linkListItems.insert(link, at: 0) - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - - let navigationController = controller.navigationController - controller.push(folderInviteLinkListController(context: component.context, filterId: folderId, title: title, allPeerIds: peers.map(\.id), currentInvitation: link, linkUpdated: { [weak self] updatedLink in - guard let self else { - return - } - if let index = self.linkListItems.firstIndex(where: { $0.link == link.link }) { - if let updatedLink { - self.linkListItems[index] = updatedLink - } else { - self.linkListItems.remove(at: index) - } - } else { - if let updatedLink { - self.linkListItems.insert(updatedLink, at: 0) - } - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - }, presentController: { [weak navigationController] c in - (navigationController?.topViewController as? ViewController)?.present(c, in: .window(.root)) - })) - - controller.dismiss() - }, error: { [weak self] error in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let context = component.context - let navigationController = controller.navigationController as? NavigationController - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - switch error { - case .generic: - text = presentationData.strings.ChatListFilter_CreateLinkUnknownError - case let .sharedFolderLimitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders)) - }) - - controller.push(limitController) - - return - case let .limitExceeded(limit, _): - let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .linksPerSharedFolder)) - }) - controller.push(limitController) - - return - case let .tooManyChannels(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .chatsPerFolder, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder)) - }) - controller.push(limitController) - controller.dismiss() - - return - case let .tooManyChannelsInAccount(limit, _): - let limitController = PremiumLimitScreen(context: component.context, subject: .channels, count: limit, action: { [weak navigationController] in - guard let navigationController else { - return - } - navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels)) - }) - controller.push(limitController) - controller.dismiss() - - return - case .someUserTooManyChannels: - text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit - } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }) - } - }) - }) - } } func makeView() -> View { @@ -1422,22 +1053,144 @@ import AnimatedCounterComponent } public class ShareWithPeersScreen: ViewControllerComponentContainer { - public enum Subject: Equatable { - case slug(String) - case updates(ChatFolderUpdates) - case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id]) - case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink]) + public final class State { + let peers: [EnginePeer] + let presences: [EnginePeer.Id: EnginePeer.Presence] + + fileprivate init( + peers: [EnginePeer], + presences: [EnginePeer.Id: EnginePeer.Presence] + ) { + self.peers = peers + self.presences = presences + } + } + + public final class StateContext { + public enum Subject: Equatable { + case contacts + case search(String) + } + + fileprivate var stateValue: State? + + public let subject: Subject + + private var stateDisposable: Disposable? + private let stateSubject = Promise() + public var state: Signal { + return self.stateSubject.get() + } + private let readySubject = ValuePromise(false, ignoreRepeated: true) + public var ready: Signal { + return self.readySubject.get() + } + + public init( + context: AccountContext, + subject: Subject = .contacts + ) { + self.subject = subject + + switch subject { + case .contacts: + self.stateDisposable = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) + ) + |> deliverOnMainQueue).start(next: { [weak self] contactList in + guard let self else { + return + } + + let state = State( + peers: contactList.peers.sorted(by: { lhs, rhs in + let lhsPresence = contactList.presences[lhs.id] + let rhsPresence = contactList.presences[rhs.id] + + if let lhsPresence, let rhsPresence { + return lhsPresence.status > rhsPresence.status + } else if lhsPresence != nil { + return true + } else if rhsPresence != nil { + return false + } else { + return lhs.id < rhs.id + } + }), + presences: contactList.presences + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .search(query): + self.stateDisposable = (context.engine.contacts.searchContacts(query: query) + |> deliverOnMainQueue).start(next: { [weak self] peers, presences in + guard let self else { + return + } + + let state = State( + peers: peers, + presences: presences + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + } + } + + deinit { + self.stateDisposable?.dispose() + } } private let context: AccountContext - private var linkContentsDisposable: Disposable? private var isDismissed: Bool = false - public init(context: AccountContext, subject: Subject, contents: ChatFolderLinkContents, completion: (() -> Void)? = nil) { + public init(context: AccountContext, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void) { self.context = context - super.init(context: context, component: ShareWithPeersScreenComponent(context: context, subject: subject, linkContents: contents, completion: completion), navigationBarAppearance: .none) + var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .everyone, + title: "Everyone", + icon: "Chat List/Filters/Channel", + iconColor: .blue, + actionTitle: nil + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .contacts, + title: "Contacts", + icon: "Chat List/Tabs/IconContacts", + iconColor: .yellow, + actionTitle: nil + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .closeFriends, + title: "Close Friends", + icon: "Call/StarHighlighted", + iconColor: .green, + actionTitle: nil + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .selectedContacts, + title: "Selected Contacts", + icon: "Chat List/Filters/Group", + iconColor: .purple, + actionTitle: nil + )) + + super.init(context: context, component: ShareWithPeersScreenComponent( + context: context, + stateContext: stateContext, + categoryItems: categoryItems, + completion: completion + ), navigationBarAppearance: .none, theme: .dark) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal @@ -1451,7 +1204,6 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } deinit { - self.linkContentsDisposable?.dispose() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -1472,6 +1224,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { if !self.isDismissed { self.isDismissed = true + self.view.endEditing(true) + if let componentView = self.node.hostView.componentView as? ShareWithPeersScreenComponent.View { componentView.animateOut(completion: { [weak self] in completion?() @@ -1483,26 +1237,3 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } } } - -private final class LinkListContextExtractedContentSource: ContextExtractedContentSource { - let keepInPlace: Bool = false - let ignoreContentTouches: Bool = false - let blurBackground: Bool = true - - //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center - - private let contentView: ContextExtractedContentContainingView - - init(contentView: ContextExtractedContentContainingView) { - self.contentView = contentView - } - - func takeView() -> ContextControllerTakeViewInfo? { - return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) - } - - func putBack() -> ContextControllerPutBackViewInfo? { - return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) - } -} -*/ diff --git a/submodules/TelegramUI/Components/TokenListTextField/BUILD b/submodules/TelegramUI/Components/TokenListTextField/BUILD new file mode 100644 index 0000000000..31308b9b22 --- /dev/null +++ b/submodules/TelegramUI/Components/TokenListTextField/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "TokenListTextField", + module_name = "TokenListTextField", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramCore", + "//submodules/AvatarNode", + "//submodules/AccountContext", + "//submodules/Components/ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift new file mode 100644 index 0000000000..81bca64a7e --- /dev/null +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -0,0 +1,534 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import TelegramPresentationData +import AvatarNode +import AccountContext + +struct EditableTokenListToken { + enum Subject { + case peer(EnginePeer) + case category(UIImage?) + } + + let id: AnyHashable + let title: String + let fixedPosition: Int? + let subject: Subject +} + +private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee)) + +private func caretAnimation() -> CAAnimation { + let animation = CAKeyframeAnimation(keyPath: "opacity") + animation.values = [1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber] + let firstDuration = 0.3 + let secondDuration = 0.25 + let restDuration = 0.35 + let duration = firstDuration + secondDuration + restDuration + let keyTimes: [NSNumber] = [0.0 as NSNumber, (firstDuration / duration) as NSNumber, ((firstDuration + secondDuration) / duration) as NSNumber, ((firstDuration + secondDuration + restDuration) / duration) as NSNumber] + + animation.keyTimes = keyTimes + animation.duration = duration + animation.repeatCount = Float.greatestFiniteMagnitude + return animation +} + +private func generateRemoveIcon(_ color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.0 - UIScreenPixel) + context.setLineCap(.round) + + let length: CGFloat = 8.0 + context.move(to: CGPoint(x: 7.0, y: 7.0)) + context.addLine(to: CGPoint(x: 7.0 + length, y: 7.0 + length)) + context.strokePath() + + context.move(to: CGPoint(x: 7.0 + length, y: 7.0)) + context.addLine(to: CGPoint(x: 7.0, y: 7.0 + length)) + context.strokePath() + }) +} + +final class EditableTokenListNodeTheme { + let backgroundColor: UIColor + let separatorColor: UIColor + let placeholderTextColor: UIColor + let primaryTextColor: UIColor + let tokenBackgroundColor: UIColor + let selectedTextColor: UIColor + let selectedBackgroundColor: UIColor + let accentColor: UIColor + let keyboardColor: PresentationThemeKeyboardColor + + init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.placeholderTextColor = placeholderTextColor + self.primaryTextColor = primaryTextColor + self.tokenBackgroundColor = tokenBackgroundColor + self.selectedTextColor = selectedTextColor + self.selectedBackgroundColor = selectedBackgroundColor + self.accentColor = accentColor + self.keyboardColor = keyboardColor + } +} + +private final class TokenNode: ASDisplayNode { + private let context: AccountContext + private let presentationTheme: PresentationTheme + + let theme: EditableTokenListNodeTheme + let token: EditableTokenListToken + let avatarNode: AvatarNode + let categoryAvatarNode: ASImageNode + let removeIconNode: ASImageNode + let titleNode: ASTextNode + let backgroundNode: ASImageNode + let selectedBackgroundNode: ASImageNode + var isSelected: Bool = false + // didSet { + // if self.isSelected != oldValue { + // self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) + // self.titleNode.redrawIfPossible() + // self.backgroundNode.isHidden = self.isSelected + // self.selectedBackgroundNode.isHidden = !self.isSelected + // + // self.avatarNode.isHidden = self.isSelected + // self.categoryAvatarNode.isHidden = self.isSelected + // self.removeIconNode.isHidden = !self.isSelected + // } + // } + // } + + init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, token: EditableTokenListToken, isSelected: Bool) { + self.context = context + self.presentationTheme = presentationTheme + self.theme = theme + self.token = token + self.titleNode = ASTextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + self.titleNode.maximumNumberOfLines = 1 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) + self.categoryAvatarNode = ASImageNode() + self.categoryAvatarNode.displaysAsynchronously = false + self.categoryAvatarNode.displayWithoutProcessing = true + + self.removeIconNode = ASImageNode() + self.removeIconNode.alpha = 0.0 + self.removeIconNode.displaysAsynchronously = false + self.removeIconNode.displayWithoutProcessing = true + self.removeIconNode.image = generateRemoveIcon(theme.selectedTextColor) + + let cornerRadius: CGFloat + switch token.subject { + case .peer: + cornerRadius = 24.0 + case .category: + cornerRadius = 14.0 + } + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.tokenBackgroundColor) + + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.alpha = 0.0 + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.displayWithoutProcessing = true + self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.selectedBackgroundColor) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.selectedBackgroundNode) + self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) + self.addSubnode(self.titleNode) + self.addSubnode(self.removeIconNode) + + switch token.subject { + case let .peer(peer): + self.addSubnode(self.avatarNode) + self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer) + case let .category(image): + self.addSubnode(self.categoryAvatarNode) + self.categoryAvatarNode.image = image + } + + self.updateIsSelected(isSelected, animated: false) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height)) + return CGSize(width: 22.0 + titleSize.width + 16.0, height: 28.0) + } + + override func layout() { + let titleSize = self.titleNode.calculatedSize + if titleSize.width.isZero { + return + } + self.backgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0) + self.selectedBackgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0) + self.avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 22.0, height: 22.0)) + self.categoryAvatarNode.frame = self.avatarNode.frame + self.removeIconNode.frame = self.avatarNode.frame + + self.titleNode.frame = CGRect(origin: CGPoint(x: 29.0, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) + } + + func updateIsSelected(_ isSelected: Bool, animated: Bool) { + guard self.isSelected != isSelected else { + return + } + self.isSelected = isSelected + + self.avatarNode.alpha = isSelected ? 0.0 : 1.0 + self.categoryAvatarNode.alpha = isSelected ? 0.0 : 1.0 + self.removeIconNode.alpha = isSelected ? 1.0 : 0.0 + + if animated { + if isSelected { + self.selectedBackgroundNode.alpha = 1.0 + self.selectedBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + self.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.avatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + + self.categoryAvatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.categoryAvatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + + self.removeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.removeIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } else { + self.selectedBackgroundNode.alpha = 0.0 + self.selectedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.avatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + + self.categoryAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.categoryAvatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + + self.removeIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.removeIconNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) + } + + if let snapshotView = self.titleNode.view.snapshotContentTree() { + self.titleNode.view.superview?.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) + self.titleNode.redrawIfPossible() + } +} + +private final class CaretIndicatorNode: ASImageNode { + override func willEnterHierarchy() { + super.willEnterHierarchy() + + if self.layer.animation(forKey: "blink") == nil { + self.layer.add(caretAnimation(), forKey: "blink") + } + } +} + +final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { + private let context: AccountContext + private let presentationTheme: PresentationTheme + + private let theme: EditableTokenListNodeTheme + private let backgroundNode: NavigationBackgroundNode + private let scrollNode: ASScrollNode + private let placeholderNode: ASTextNode + private var tokenNodes: [TokenNode] = [] + private let separatorNode: ASDisplayNode + private let textFieldScrollNode: ASScrollNode + private let textFieldNode: TextFieldNode + private let caretIndicatorNode: CaretIndicatorNode + private var selectedTokenId: AnyHashable? + + var isFocused: Bool { + return self.textFieldNode.view.isFirstResponder + } + + var textUpdated: ((String) -> Void)? + var deleteToken: ((AnyHashable) -> Void)? + var textReturned: (() -> Void)? + var isFirstResponderChanged: (() -> Void)? + + init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String) { + self.context = context + self.presentationTheme = presentationTheme + self.theme = theme + + self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor) + + self.scrollNode = ASScrollNode() + self.scrollNode.view.alwaysBounceVertical = true + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.maximumNumberOfLines = 1 + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) + + self.textFieldScrollNode = ASScrollNode() + + self.textFieldNode = TextFieldNode() + self.textFieldNode.textField.font = Font.regular(15.0) + self.textFieldNode.textField.textColor = theme.primaryTextColor + self.textFieldNode.textField.autocorrectionType = .no + self.textFieldNode.textField.returnKeyType = .done + self.textFieldNode.textField.keyboardAppearance = theme.keyboardColor.keyboardAppearance + self.textFieldNode.textField.tintColor = theme.accentColor + + self.caretIndicatorNode = CaretIndicatorNode() + self.caretIndicatorNode.isLayerBacked = true + self.caretIndicatorNode.displayWithoutProcessing = true + self.caretIndicatorNode.displaysAsynchronously = false + self.caretIndicatorNode.image = caretIndicatorImage + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.separatorColor + + super.init() + self.addSubnode(self.backgroundNode) + self.addSubnode(self.scrollNode) + + self.addSubnode(self.separatorNode) + self.scrollNode.addSubnode(self.placeholderNode) + self.scrollNode.addSubnode(self.textFieldScrollNode) + self.textFieldScrollNode.addSubnode(self.textFieldNode) + //self.scrollNode.addSubnode(self.caretIndicatorNode) + self.clipsToBounds = true + + self.textFieldNode.textField.delegate = self + self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + self.textFieldNode.textField.didDeleteBackwardWhileEmpty = { [weak self] in + if let strongSelf = self { + if let selectedTokenId = strongSelf.selectedTokenId { + strongSelf.deleteToken?(selectedTokenId) + strongSelf.updateSelectedTokenId(nil) + } else if let tokenNode = strongSelf.tokenNodes.last { + strongSelf.updateSelectedTokenId(tokenNode.token.id, animated: true) + } + } + } + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let validTokens = Set(tokens.map { $0.id }) + + for i in (0 ..< self.tokenNodes.count).reversed() { + let tokenNode = tokenNodes[i] + if !validTokens.contains(tokenNode.token.id) { + self.tokenNodes.remove(at: i) + if case .immediate = transition { + tokenNode.removeFromSupernode() + } else { + tokenNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tokenNode] _ in + tokenNode?.removeFromSupernode() + }) + tokenNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) + } + } + } + + if let selectedTokenId = self.selectedTokenId, !validTokens.contains(selectedTokenId) { + self.selectedTokenId = nil + } + + let sideInset: CGFloat = 12.0 + leftInset + let verticalInset: CGFloat = 6.0 + + + var animationDelay = 0.0 + var currentOffset = CGPoint(x: sideInset, y: verticalInset) + for token in tokens { + var currentNode: TokenNode? + for node in self.tokenNodes { + if node.token.id == token.id { + currentNode = node + break + } + } + let tokenNode: TokenNode + var animateIn = false + if let currentNode = currentNode { + tokenNode = currentNode + } else { + tokenNode = TokenNode(context: self.context, presentationTheme: self.presentationTheme, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!) + self.tokenNodes.append(tokenNode) + self.scrollNode.addSubnode(tokenNode) + animateIn = true + } + + let tokenSize = tokenNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude)) + if tokenSize.width + currentOffset.x >= width - sideInset && !currentOffset.x.isEqual(to: sideInset) { + currentOffset.x = sideInset + currentOffset.y += tokenSize.height + } + let tokenFrame = CGRect(origin: CGPoint(x: currentOffset.x, y: currentOffset.y), size: tokenSize) + currentOffset.x += ceil(tokenSize.width) + + if animateIn { + tokenNode.frame = tokenFrame + tokenNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + tokenNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) + } else { + if case .immediate = transition { + transition.updateFrame(node: tokenNode, frame: tokenFrame) + } else { + let previousFrame = tokenNode.frame + if !previousFrame.origin.y.isEqual(to: tokenFrame.origin.y) && previousFrame.size.width.isEqual(to: tokenFrame.size.width) { + let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY) + let initialEndPosition = CGPoint(x: previousFrame.midY > tokenFrame.midY ? -previousFrame.size.width / 2.0 : width, y: previousFrame.midY) + let targetStartPosition = CGPoint(x: (previousFrame.midY > tokenFrame.midY ? (width + tokenFrame.size.width) : -tokenFrame.size.width), y: tokenFrame.midY) + let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY) + tokenNode.frame = tokenFrame + + let initialAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: initialEndPosition), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.12, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil) + let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: targetStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil) + tokenNode.layer.animateGroup([initialAnimation, targetAnimation], key: "slide") + animationDelay += 0.025 + } else { + if !previousFrame.size.width.isEqual(to: tokenFrame.size.width) { + tokenNode.frame = tokenFrame + } else { + let initialStartPosition = CGPoint(x: previousFrame.midX, y: previousFrame.midY) + let targetEndPosition = CGPoint(x: tokenFrame.midX, y: tokenFrame.midY) + tokenNode.frame = tokenFrame + + let targetAnimation = tokenNode.layer.makeAnimation(from: NSValue(cgPoint: initialStartPosition), to: NSValue(cgPoint: targetEndPosition), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.2 + animationDelay, mediaTimingFunction: nil, removeOnCompletion: true, additive: false, completion: nil) + tokenNode.layer.animateGroup([targetAnimation], key: "slide") + animationDelay += 0.025 + } + } + } + } + } + + let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude)) + if width - currentOffset.x < placeholderSize.width { + currentOffset.y += 28.0 + currentOffset.x = sideInset + } + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize)) + + let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0)) + let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel)) + if case .immediate = transition { + transition.updateFrame(node: self.textFieldScrollNode, frame: textNodeFrame) + transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size)) + transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame) + } else { + let previousFrame = self.textFieldScrollNode.frame + self.textFieldScrollNode.frame = textNodeFrame + self.textFieldScrollNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring) + + transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size)) + + let previousCaretFrame = self.caretIndicatorNode.frame + self.caretIndicatorNode.frame = caretNodeFrame + self.caretIndicatorNode.layer.animateFrame(from: previousCaretFrame, to: caretNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring) + } + + let previousContentHeight = self.scrollNode.view.contentSize.height + let contentHeight = currentOffset.y + 29.0 + verticalInset + let nodeHeight = min(contentHeight, 110.0) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))) + + if !abs(previousContentHeight - contentHeight).isLess(than: CGFloat.ulpOfOne) { + let contentOffset = CGPoint(x: 0.0, y: max(0.0, contentHeight - nodeHeight)) + if case .immediate = transition { + self.scrollNode.view.contentOffset = contentOffset + } else { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: self.scrollNode.bounds.size)) + } + } + self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))) + self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) + + return nodeHeight + } + + @objc func textFieldChanged(_ textField: UITextField) { + let text = textField.text ?? "" + self.placeholderNode.isHidden = !text.isEmpty + self.updateSelectedTokenId(nil) + self.textUpdated?(text) + if !text.isEmpty { + self.scrollNode.view.scrollRectToVisible(textFieldScrollNode.frame.offsetBy(dx: 0.0, dy: 7.0), animated: true) + } + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.textReturned?() + return false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + self.isFirstResponderChanged?() + /*if self.caretIndicatorNode.supernode == self { + self.caretIndicatorNode.removeFromSupernode() + }*/ + } + + func textFieldDidEndEditing(_ textField: UITextField) { + self.isFirstResponderChanged?() + /*if self.caretIndicatorNode.supernode != self.scrollNode { + self.scrollNode.addSubnode(self.caretIndicatorNode) + }*/ + } + + func setText(_ text: String) { + self.textFieldNode.textField.text = text + self.textFieldChanged(self.textFieldNode.textField) + } + + private func updateSelectedTokenId(_ id: AnyHashable?, animated: Bool = false) { + self.selectedTokenId = id + for tokenNode in self.tokenNodes { + tokenNode.updateIsSelected(id == tokenNode.token.id, animated: animated) + } + if id != nil && !self.textFieldNode.textField.isFirstResponder { + self.textFieldNode.textField.becomeFirstResponder() + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self.view) + for tokenNode in self.tokenNodes { + let convertedPoint = self.view.convert(point, to: tokenNode.view) + if tokenNode.bounds.contains(convertedPoint) { + if tokenNode.isSelected { + self.deleteToken?(tokenNode.token.id) + } else { + self.updateSelectedTokenId(tokenNode.token.id, animated: true) + } + break + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift new file mode 100644 index 0000000000..a906a60105 --- /dev/null +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/TokenListTextField.swift @@ -0,0 +1,254 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AccountContext +import TelegramPresentationData +import AsyncDisplayKit +import TelegramCore +import ComponentDisplayAdapters + +public final class TokenListTextField: Component { + public final class ExternalState { + public fileprivate(set) var isFocused: Bool = false + public fileprivate(set) var text: String = "" + + public init() { + } + } + + public final class Token: Equatable { + public enum Content: Equatable { + case peer(EnginePeer) + case category(UIImage?) + + public static func ==(lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .peer(peer): + if case .peer(peer) = rhs { + return true + } else { + return false + } + case let .category(lhsImage): + if case let .category(rhsImage) = rhs, lhsImage === rhsImage { + return true + } else { + return false + } + } + } + } + + public let id: AnyHashable + public let title: String + public let fixedPosition: Int? + public let content: Content + + public init( + id: AnyHashable, + title: String, + fixedPosition: Int?, + content: Content + ) { + self.id = id + self.title = title + self.fixedPosition = fixedPosition + self.content = content + } + + public static func ==(lhs: Token, rhs: Token) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.fixedPosition != rhs.fixedPosition { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + } + + public let externalState: ExternalState + public let context: AccountContext + public let theme: PresentationTheme + public let placeholder: String + public let tokens: [Token] + public let sideInset: CGFloat + public let deleteToken: (AnyHashable) -> Void + + public init( + externalState: ExternalState, + context: AccountContext, + theme: PresentationTheme, + placeholder: String, + tokens: [Token], + sideInset: CGFloat, + deleteToken: @escaping (AnyHashable) -> Void + ) { + self.externalState = externalState + self.context = context + self.theme = theme + self.placeholder = placeholder + self.tokens = tokens + self.sideInset = sideInset + self.deleteToken = deleteToken + } + + public static func ==(lhs: TokenListTextField, rhs: TokenListTextField) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.placeholder != rhs.placeholder { + return false + } + if lhs.tokens != rhs.tokens { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + return true + } + + public final class View: UIView { + private var tokenListNode: EditableTokenListNode? + + private var tokenListText: String = "" + + private var component: TokenListTextField? + private weak var componentState: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result != nil { + return result + } + + return nil + } + + public func clearText() { + if let tokenListNode = self.tokenListNode { + tokenListNode.setText("") + } + } + + func update(component: TokenListTextField, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.componentState = state + + let tokenListNode: EditableTokenListNode + if let current = self.tokenListNode { + tokenListNode = current + } else { + tokenListNode = EditableTokenListNode( + context: component.context, + presentationTheme: component.theme, + theme: EditableTokenListNodeTheme( + backgroundColor: .clear, + separatorColor: component.theme.rootController.navigationBar.separatorColor, + placeholderTextColor: component.theme.list.itemPlaceholderTextColor, + primaryTextColor: component.theme.list.itemPrimaryTextColor, + tokenBackgroundColor: component.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), + selectedTextColor: component.theme.list.itemCheckColors.foregroundColor, + selectedBackgroundColor: component.theme.list.itemCheckColors.fillColor, + accentColor: component.theme.list.itemAccentColor, + keyboardColor: component.theme.rootController.keyboardColor + ), + placeholder: component.placeholder + ) + self.tokenListNode = tokenListNode + self.addSubnode(tokenListNode) + + tokenListNode.isFirstResponderChanged = { [weak self] in + guard let self else { + return + } + self.componentState?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + + tokenListNode.textUpdated = { [weak self] text in + guard let self else { + return + } + self.tokenListText = text + self.componentState?.updated(transition: .immediate) + } + + tokenListNode.textReturned = { [weak self] in + guard let self else { + return + } + self.tokenListNode?.view.endEditing(true) + } + + tokenListNode.deleteToken = { [weak self] id in + guard let self, let component = self.component else { + return + } + component.deleteToken(id) + } + } + + let mappedTokens = component.tokens.map { token -> EditableTokenListToken in + let mappedSubject: EditableTokenListToken.Subject + switch token.content { + case let .peer(peer): + mappedSubject = .peer(peer) + case let .category(image): + mappedSubject = .category(image) + } + + return EditableTokenListToken( + id: token.id, + title: token.title, + fixedPosition: token.fixedPosition, + subject: mappedSubject + ) + } + + let height = tokenListNode.updateLayout( + tokens: mappedTokens, + width: availableSize.width, + leftInset: component.sideInset, + rightInset: component.sideInset, + transition: transition.containedViewLayoutTransition + ) + let size = CGSize(width: availableSize.width, height: height) + transition.containedViewLayoutTransition.updateFrame(node: tokenListNode, frame: CGRect(origin: CGPoint(), size: size)) + + component.externalState.isFocused = tokenListNode.isFocused + component.externalState.text = self.tokenListText + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 09996c5c3e..863fbea304 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -24,6 +24,7 @@ import LegacyMediaPickerUI import LegacyCamera import AvatarNode import LocalMediaResources +import ShareWithPeersScreen private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -349,57 +350,59 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } }, completion: { mediaResult, commit in - let privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) -// if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) { -// privacy.base = .everyone -// } else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) { -// privacy.base = .contacts -// } else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) { -// privacy.base = .closeFriends -// } -// privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in -// switch id { -// case let .peer(peerId): -// return peerId -// default: -// return nil -// } -// } - - if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { - switch mediaResult { - case let .image(image, dimensions, caption): - if let data = image.jpegData(compressionQuality: 0.8) { - storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) - } - case let .video(content, _, values, duration, dimensions, caption): - let adjustments: VideoMediaResourceAdjustments - if let valuesData = try? JSONEncoder().encode(values) { - let data = MemoryBuffer(data: valuesData) - let digest = MemoryBuffer(data: data.md5Digest()) - adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) - - let resource: TelegramMediaResource - switch content { - case let .imageFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .videoFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .asset(localIdentifier): - resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) - } - storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) - } + let stateContext = ShareWithPeersScreen.StateContext(context: self.context) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return } - } - dismissCameraImpl?() - commit() + guard let controller = self.viewControllers.last as? ViewController else { + return + } + + controller.push(ShareWithPeersScreen(context: self.context, stateContext: stateContext, completion: { [weak self] privacy in + guard let self else { + dismissCameraImpl?() + commit() + return + } + + if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { + switch mediaResult { + case let .image(image, dimensions, caption): + if let data = image.jpegData(compressionQuality: 0.8) { + storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: caption?.string ?? "", entities: [], privacy: privacy) + Queue.mainQueue().after(0.3, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } + case let .video(content, _, values, duration, dimensions, caption): + let adjustments: VideoMediaResourceAdjustments + if let valuesData = try? JSONEncoder().encode(values) { + let data = MemoryBuffer(data: valuesData) + let digest = MemoryBuffer(data: data.md5Digest()) + adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) + + let resource: TelegramMediaResource + switch content { + case let .imageFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .videoFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .asset(localIdentifier): + resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) + } + storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy) + Queue.mainQueue().after(0.3, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } + } + } + + dismissCameraImpl?() + commit() + })) + }) }) controller.sourceHint = .camera controller.cancelled = {