mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Send invite link UI
This commit is contained in:
parent
774cca56cb
commit
40fb4a2ded
@ -10,6 +10,7 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
|
||||
public let title: String?
|
||||
public let label: String?
|
||||
public let badge: String?
|
||||
public let icon: UIImage?
|
||||
public let theme: SolidRoundedButtonTheme
|
||||
public let font: SolidRoundedButtonFont
|
||||
@ -28,6 +29,7 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
public init(
|
||||
title: String? = nil,
|
||||
label: String? = nil,
|
||||
badge: String? = nil,
|
||||
icon: UIImage? = nil,
|
||||
theme: SolidRoundedButtonTheme,
|
||||
font: SolidRoundedButtonFont = .bold,
|
||||
@ -45,6 +47,7 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
) {
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.badge = badge
|
||||
self.icon = icon
|
||||
self.theme = theme
|
||||
self.font = font
|
||||
@ -68,6 +71,9 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
if lhs.label != rhs.label {
|
||||
return false
|
||||
}
|
||||
if lhs.badge != rhs.badge {
|
||||
return false
|
||||
}
|
||||
if lhs.icon !== rhs.icon {
|
||||
return false
|
||||
}
|
||||
@ -121,6 +127,7 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
let button = SolidRoundedButtonView(
|
||||
title: component.title,
|
||||
label: component.label,
|
||||
badge: component.badge,
|
||||
icon: component.icon,
|
||||
theme: component.theme,
|
||||
font: component.font,
|
||||
@ -141,6 +148,7 @@ public final class SolidRoundedButtonComponent: Component {
|
||||
if let button = self.button {
|
||||
button.title = component.title
|
||||
button.label = component.label
|
||||
button.badge = component.badge
|
||||
button.iconPosition = component.iconPosition
|
||||
button.iconSpacing = component.iconSpacing
|
||||
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
|
||||
|
@ -69,6 +69,93 @@ public enum SolidRoundedButtonProgressType {
|
||||
case embedded
|
||||
}
|
||||
|
||||
private final class BadgeNode: ASDisplayNode {
|
||||
private var fillColor: UIColor
|
||||
private var strokeColor: UIColor
|
||||
private var textColor: UIColor
|
||||
|
||||
private let textNode: ImmediateTextNode
|
||||
private let backgroundNode: ASImageNode
|
||||
|
||||
private let font: UIFont = Font.with(size: 15.0, design: .round, weight: .bold)
|
||||
|
||||
var text: String = "" {
|
||||
didSet {
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
||||
self.invalidateCalculatedLayout()
|
||||
}
|
||||
}
|
||||
|
||||
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.textColor = textColor
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: nil, strokeWidth: 1.0)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.textColor = textColor
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
||||
}
|
||||
|
||||
func animateBump(incremented: Bool) {
|
||||
if incremented {
|
||||
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
||||
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2)
|
||||
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in
|
||||
if finished {
|
||||
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
||||
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
|
||||
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
||||
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8)
|
||||
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in
|
||||
if finished {
|
||||
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
||||
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
|
||||
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
|
||||
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
|
||||
self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
|
||||
}
|
||||
|
||||
func update(_ constrainedSize: CGSize) -> CGSize {
|
||||
let badgeSize = self.textNode.updateLayout(constrainedSize)
|
||||
let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 8.0), height: 18.0)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
|
||||
self.backgroundNode.frame = backgroundFrame
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize)
|
||||
|
||||
return backgroundSize
|
||||
}
|
||||
}
|
||||
|
||||
public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
private var theme: SolidRoundedButtonTheme
|
||||
private var font: SolidRoundedButtonFont
|
||||
@ -89,6 +176,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
private let iconNode: ASImageNode
|
||||
private var animationNode: SimpleAnimationNode?
|
||||
private var progressNode: ASImageNode?
|
||||
private var badgeNode: BadgeNode?
|
||||
|
||||
private let buttonHeight: CGFloat
|
||||
private let buttonCornerRadius: CGFloat
|
||||
@ -123,6 +211,15 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public var badge: String? {
|
||||
didSet {
|
||||
self.updateAccessibilityLabels()
|
||||
if let width = self.validLayout {
|
||||
_ = self.updateLayout(width: width, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var icon: UIImage? {
|
||||
didSet {
|
||||
self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor)
|
||||
@ -630,6 +727,23 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
|
||||
}
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
if let badge = self.badge {
|
||||
let badgeNode: BadgeNode
|
||||
if let current = self.badgeNode {
|
||||
badgeNode = current
|
||||
} else {
|
||||
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
|
||||
self.badgeNode = badgeNode
|
||||
self.addSubnode(badgeNode)
|
||||
}
|
||||
badgeNode.text = badge
|
||||
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
|
||||
transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
|
||||
} else if let badgeNode = self.badgeNode {
|
||||
self.badgeNode = nil
|
||||
badgeNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if self.subtitle != self.subtitleNode.attributedText?.string {
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
|
||||
}
|
||||
@ -766,6 +880,7 @@ public final class SolidRoundedButtonView: UIView {
|
||||
private let iconNode: UIImageView
|
||||
private var animationNode: SimpleAnimationNode?
|
||||
private var progressNode: UIImageView?
|
||||
private var badgeNode: BadgeNode?
|
||||
|
||||
private let buttonHeight: CGFloat
|
||||
private let buttonCornerRadius: CGFloat
|
||||
@ -800,6 +915,15 @@ public final class SolidRoundedButtonView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public var badge: String? {
|
||||
didSet {
|
||||
self.updateAccessibilityLabels()
|
||||
if let width = self.validLayout {
|
||||
_ = self.updateLayout(width: width, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAccessibilityLabels() {
|
||||
self.accessibilityLabel = (self.title ?? "") + " " + (self.subtitle ?? "")
|
||||
self.accessibilityValue = self.label
|
||||
@ -900,7 +1024,7 @@ public final class SolidRoundedButtonView: UIView {
|
||||
|
||||
public var progressType: SolidRoundedButtonProgressType = .fullSize
|
||||
|
||||
public init(title: String? = nil, label: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
||||
public init(title: String? = nil, label: String? = nil, badge: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
||||
self.theme = theme
|
||||
self.font = font
|
||||
self.fontSize = fontSize
|
||||
@ -908,6 +1032,7 @@ public final class SolidRoundedButtonView: UIView {
|
||||
self.buttonCornerRadius = cornerRadius
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.badge = badge
|
||||
self.gloss = gloss
|
||||
|
||||
self.buttonBackgroundNode = UIImageView()
|
||||
@ -1325,6 +1450,23 @@ public final class SolidRoundedButtonView: UIView {
|
||||
}
|
||||
transition.updateFrame(view: self.titleNode, frame: titleFrame)
|
||||
|
||||
if let badge = self.badge {
|
||||
let badgeNode: BadgeNode
|
||||
if let current = self.badgeNode {
|
||||
badgeNode = current
|
||||
} else {
|
||||
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
|
||||
self.badgeNode = badgeNode
|
||||
self.addSubnode(badgeNode)
|
||||
}
|
||||
badgeNode.text = badge
|
||||
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
|
||||
transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
|
||||
} else if let badgeNode = self.badgeNode {
|
||||
self.badgeNode = nil
|
||||
badgeNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if self.subtitle != self.subtitleNode.attributedText?.string {
|
||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
|
||||
}
|
||||
|
@ -357,6 +357,7 @@ swift_library(
|
||||
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
37
submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD
Normal file
37
submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD
Normal file
@ -0,0 +1,37 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SendInviteLinkScreen",
|
||||
module_name = "SendInviteLinkScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/PeerPresenceStatusManager",
|
||||
"//submodules/UndoUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,328 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import Postbox
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import PeerPresenceStatusManager
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
|
||||
private func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for gesture in gestureRecognizers {
|
||||
if let gesture = gesture as? ContextGesture {
|
||||
gesture.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let peer: EnginePeer?
|
||||
let presence: EnginePeer.Presence?
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
peer: EnginePeer?,
|
||||
presence: EnginePeer.Presence?,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.peer = peer
|
||||
self.presence = presence
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
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<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var component: PeerListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var statusManager: PeerPresenceStatusManager?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.addSubview(self.containerButton)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
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, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer)
|
||||
}
|
||||
|
||||
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if let presence = component.presence {
|
||||
let statusManager: PeerPresenceStatusManager
|
||||
if let current = self.statusManager {
|
||||
statusManager = current
|
||||
} else {
|
||||
statusManager = PeerPresenceStatusManager(update: { [weak state] in
|
||||
state?.updated(transition: .immediate)
|
||||
})
|
||||
self.statusManager = statusManager
|
||||
}
|
||||
statusManager.reset(presence: presence)
|
||||
} else {
|
||||
self.statusManager = nil
|
||||
}
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
|
||||
let height: CGFloat = 60.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
let leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||
let avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
if case let .editing(isSelected) = component.selectionState {
|
||||
rightInset += 48.0
|
||||
|
||||
let checkSize: CGFloat = 22.0
|
||||
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
if themeUpdated {
|
||||
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
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: availableSize.width - rightInset + floor((48.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 self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} 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))
|
||||
}
|
||||
}
|
||||
|
||||
let labelData: (String, Bool)
|
||||
if let presence = component.presence {
|
||||
labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(Date().timeIntervalSince1970))
|
||||
} else {
|
||||
labelData = (component.strings.LastSeen_Offline, 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
|
||||
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||
|
||||
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<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -0,0 +1,636 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import Postbox
|
||||
import SolidRoundedButtonComponent
|
||||
import PresentationDataUtils
|
||||
import Markdown
|
||||
import UndoUI
|
||||
|
||||
private final class SendInviteLinkScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let link: String
|
||||
let peers: [EnginePeer]
|
||||
let peerPresences: [EnginePeer.Id: EnginePeer.Presence]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
link: String,
|
||||
peers: [EnginePeer],
|
||||
peerPresences: [EnginePeer.Id: EnginePeer.Presence]
|
||||
) {
|
||||
self.context = context
|
||||
self.link = link
|
||||
self.peers = peers
|
||||
self.peerPresences = peerPresences
|
||||
}
|
||||
|
||||
static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.link != rhs.link {
|
||||
return false
|
||||
}
|
||||
if lhs.peers != rhs.peers {
|
||||
return false
|
||||
}
|
||||
if lhs.peerPresences != rhs.peerPresences {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var containerSize: CGSize
|
||||
var containerInset: CGFloat
|
||||
var bottomInset: CGFloat
|
||||
var topInset: CGFloat
|
||||
|
||||
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
|
||||
self.containerSize = containerSize
|
||||
self.containerInset = containerInset
|
||||
self.bottomInset = bottomInset
|
||||
self.topInset = topInset
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let dimView: UIView
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let navigationBarContainer: SparseContainerView
|
||||
private let scrollView: ScrollView
|
||||
private let scrollContentClippingView: SparseContainerView
|
||||
private let scrollContentView: UIView
|
||||
|
||||
private let iconBackgroundView: UIView
|
||||
private let iconView: UIImageView
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let leftButton = ComponentView<Empty>()
|
||||
private let descriptionText = ComponentView<Empty>()
|
||||
private let actionButton = ComponentView<Empty>()
|
||||
|
||||
private let itemContainerView: UIView
|
||||
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
private var selectedItems = Set<EnginePeer.Id>()
|
||||
|
||||
private let bottomOverscrollLimit: CGFloat
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var component: SendInviteLinkScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var topOffsetDistance: CGFloat?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.bottomOverscrollLimit = 200.0
|
||||
|
||||
self.dimView = UIView()
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
self.backgroundLayer.cornerRadius = 10.0
|
||||
|
||||
self.navigationBarContainer = SparseContainerView()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
self.scrollContentClippingView = SparseContainerView()
|
||||
self.scrollContentClippingView.clipsToBounds = true
|
||||
|
||||
self.scrollContentView = UIView()
|
||||
|
||||
self.iconBackgroundView = UIView()
|
||||
self.iconView = UIImageView(image: UIImage(bundleImageName: "Chat/Links/LargeLink")?.withRenderingMode(.alwaysTemplate))
|
||||
|
||||
self.itemContainerView = UIView()
|
||||
self.itemContainerView.clipsToBounds = true
|
||||
self.itemContainerView.layer.cornerRadius = 10.0
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.dimView)
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.addSubview(self.navigationBarContainer)
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.scrollContentClippingView)
|
||||
self.scrollContentClippingView.addSubview(self.scrollView)
|
||||
|
||||
self.scrollContentView.addSubview(self.iconBackgroundView)
|
||||
self.scrollContentView.addSubview(self.iconView)
|
||||
|
||||
self.scrollView.addSubview(self.scrollContentView)
|
||||
|
||||
self.scrollContentView.addSubview(self.itemContainerView)
|
||||
|
||||
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else {
|
||||
return
|
||||
}
|
||||
|
||||
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
if !self.backgroundLayer.frame.contains(point) {
|
||||
return self.dimView
|
||||
}
|
||||
|
||||
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
|
||||
return result
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
guard let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout 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.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)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
let animateOffset: CGFloat = self.backgroundLayer.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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
let animateOffset: CGFloat = self.backgroundLayer.frame.minY
|
||||
|
||||
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
|
||||
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
if self.component == nil {
|
||||
for peer in component.peers {
|
||||
self.selectedItems.insert(peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.environment = environment
|
||||
|
||||
if themeUpdated {
|
||||
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
|
||||
self.iconBackgroundView.backgroundColor = environment.theme.list.itemCheckColors.fillColor
|
||||
self.iconView.tintColor = environment.theme.list.itemCheckColors.foregroundColor
|
||||
self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
|
||||
|
||||
var locations: [NSNumber] = []
|
||||
var colors: [CGColor] = []
|
||||
let numStops = 6
|
||||
for i in 0 ..< numStops {
|
||||
let step = CGFloat(i) / CGFloat(numStops - 1)
|
||||
locations.append(step as NSNumber)
|
||||
colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor)
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
//TODO:localize
|
||||
let leftButtonSize = self.leftButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: "Skip", font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 56.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 120.0, height: 100.0)
|
||||
)
|
||||
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)
|
||||
}
|
||||
transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
|
||||
|
||||
leftButtonView.isHidden = self.selectedItems.isEmpty ? true : false
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Invite via Link", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
||||
)
|
||||
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)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
contentHeight += 44.0
|
||||
|
||||
contentHeight += 22.0
|
||||
|
||||
let iconBackgroundSize = CGSize(width: 68.0, height: 48.0)
|
||||
let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconBackgroundSize.width) * 0.5), y: contentHeight), size: iconBackgroundSize)
|
||||
transition.setFrame(view: self.iconBackgroundView, frame: iconBackgroundFrame)
|
||||
transition.setCornerRadius(layer: self.iconBackgroundView.layer, cornerRadius: min(iconBackgroundFrame.width, iconBackgroundFrame.height) * 0.5)
|
||||
if let icon = self.iconView.image {
|
||||
let scaleFraction: CGFloat = 0.5
|
||||
let iconSize = CGSize(width: floor(icon.size.width * scaleFraction), height: floor(icon.size.height * scaleFraction))
|
||||
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor(iconBackgroundFrame.minX + (iconBackgroundFrame.width - iconSize.width) * 0.5), y: floor(iconBackgroundFrame.minY + (iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize))
|
||||
}
|
||||
|
||||
contentHeight += iconBackgroundSize.height
|
||||
|
||||
contentHeight += 26.0
|
||||
|
||||
let text: String
|
||||
if component.peers.count == 1 {
|
||||
text = "**\(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast))** restricts adding them to groups.\nYou can send them an invite link as message instead."
|
||||
} else {
|
||||
text = "**\(component.peers.count) users** restrict adding them to groups.\nYou can send them an invite link as message instead."
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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, 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)
|
||||
}
|
||||
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||
}
|
||||
|
||||
contentHeight += descriptionTextFrame.height
|
||||
contentHeight += 13.0
|
||||
|
||||
var singleItemHeight: CGFloat = 0.0
|
||||
|
||||
var itemsHeight: CGFloat = 0.0
|
||||
var validIds: [AnyHashable] = []
|
||||
for i in 0 ..< component.peers.count {
|
||||
let peer = component.peers[i]
|
||||
|
||||
for _ in 0 ..< 1 {
|
||||
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
|
||||
let id = AnyHashable(peer.id)
|
||||
validIds.append(id)
|
||||
|
||||
let item: ComponentView<Empty>
|
||||
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(PeerListItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sideInset: 0.0,
|
||||
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||
peer: peer,
|
||||
presence: component.peerPresences[peer.id],
|
||||
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id)),
|
||||
hasNext: i != component.peers.count - 1,
|
||||
action: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
transition.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 * 2.5))
|
||||
|
||||
contentHeight += itemsHeight
|
||||
contentHeight += 24.0
|
||||
initialContentHeight += 24.0
|
||||
|
||||
//TODO:localize
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(SolidRoundedButtonComponent(
|
||||
title: self.selectedItems.isEmpty ? "Skip" : "Send Invite Link",
|
||||
badge: self.selectedItems.isEmpty ? nil : "\(self.selectedItems.count)",
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 11.0,
|
||||
gloss: false,
|
||||
animationName: nil,
|
||||
iconPosition: .right,
|
||||
iconSpacing: 4.0,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
if self.selectedItems.isEmpty {
|
||||
controller.dismiss()
|
||||
} else {
|
||||
let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: component.link, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
|
||||
//TODO:localize
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: Array(component.peers.prefix(3)), title: nil, text: "Invite link sent.", customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
|
||||
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||
)
|
||||
let 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)
|
||||
}
|
||||
|
||||
contentHeight += bottomPanelHeight
|
||||
initialContentHeight += bottomPanelHeight
|
||||
|
||||
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
||||
|
||||
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
|
||||
|
||||
self.scrollContentClippingView.layer.cornerRadius = 10.0
|
||||
|
||||
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
|
||||
|
||||
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
|
||||
|
||||
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(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset + 56.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))
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset)))
|
||||
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
if resetScrolling {
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public class SendInviteLinkScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private let link: String
|
||||
private let peers: [EnginePeer]
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private var presenceDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, link: String, peers: [EnginePeer]) {
|
||||
self.context = context
|
||||
self.link = link
|
||||
self.peers = peers
|
||||
|
||||
super.init(context: context, component: SendInviteLinkScreenComponent(context: context, link: link, peers: peers, peerPresences: [:]), navigationBarAppearance: .none)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.presenceDisposable = (context.engine.data.subscribe(EngineDataMap(
|
||||
peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init(id:))
|
||||
))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presences in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var parsedPresences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||
for (id, presence) in presences {
|
||||
if let presence {
|
||||
parsedPresences[id] = presence
|
||||
}
|
||||
}
|
||||
self.updateComponent(component: AnyComponent(SendInviteLinkScreenComponent(context: context, link: link, peers: peers, peerPresences: parsedPresences)), transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presenceDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View {
|
||||
componentView.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
|
||||
if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View {
|
||||
componentView.animateOut(completion: { [weak self] in
|
||||
completion?()
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -85,6 +85,7 @@ import ChatListHeaderComponent
|
||||
import ChatControllerInteraction
|
||||
import StorageUsageScreen
|
||||
import AvatarEditorScreen
|
||||
import SendInviteLinkScreen
|
||||
|
||||
enum PeerInfoAvatarEditingMode {
|
||||
case generic
|
||||
@ -2746,7 +2747,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -10605,8 +10606,12 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in
|
||||
}
|
||||
}
|
||||
if let contactsController = contactsController as? ContactMultiselectionController {
|
||||
selectAddMemberDisposable.set((contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
|
||||
selectAddMemberDisposable.set((
|
||||
combineLatest(queue: .mainQueue(),
|
||||
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: groupPeer.id)),
|
||||
contactsController.result
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak contactsController] exportedInvitation, result in
|
||||
var peers: [ContactListPeerId] = []
|
||||
if case let .result(peerIdsValue, _) = result {
|
||||
peers = peerIdsValue
|
||||
@ -10615,6 +10620,40 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in
|
||||
contactsController?.displayProgress = true
|
||||
addMemberDisposable.set((addMembers(peers)
|
||||
|> deliverOnMainQueue).start(error: { error in
|
||||
if let exportedInvitation, let link = exportedInvitation.link {
|
||||
switch error {
|
||||
case .restricted, .notMutualContact, .kicked:
|
||||
let _ = (context.engine.data.get(
|
||||
EngineDataList(peers.compactMap { item -> EnginePeer.Id? in
|
||||
switch item {
|
||||
case let .peer(peerId):
|
||||
return peerId
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peerItems in
|
||||
let peers = peerItems.compactMap { $0 }
|
||||
if !peers.isEmpty, let contactsController, let navigationController = contactsController.navigationController as? NavigationController {
|
||||
var viewControllers = navigationController.viewControllers
|
||||
if let index = viewControllers.firstIndex(where: { $0 === contactsController }) {
|
||||
let inviteScreen = SendInviteLinkScreen(context: context, link: link, peers: peers)
|
||||
viewControllers.remove(at: index)
|
||||
viewControllers.append(inviteScreen)
|
||||
navigationController.setViewControllers(viewControllers, animated: true)
|
||||
}
|
||||
} else {
|
||||
contactsController?.dismiss()
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if peers.count == 1, case .restricted = error {
|
||||
switch peers[0] {
|
||||
case let .peer(peerId):
|
||||
@ -10634,7 +10673,7 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in
|
||||
}
|
||||
|
||||
parentController?.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
} else if case .tooMuchJoined = error {
|
||||
} else if case .tooMuchJoined = error {
|
||||
parentController?.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
} else if peers.count == 1, case .kicked = error {
|
||||
parentController?.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_AddUserKickedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
|
Loading…
x
Reference in New Issue
Block a user