Folder improvements

This commit is contained in:
Ali
2023-04-03 19:42:48 +04:00
parent 81e248889d
commit e4dc32ad11
17 changed files with 1163 additions and 172 deletions

View File

@@ -31,6 +31,8 @@ swift_library(
"//submodules/Markdown",
"//submodules/UndoUI",
"//submodules/PremiumUI",
"//submodules/QrCodeUI",
"//submodules/InviteLinksUI",
],
visibility = [
"//visibility:public",

View File

@@ -0,0 +1,196 @@
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<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@@ -17,6 +17,9 @@ import Markdown
import UndoUI
import PremiumUI
import ButtonComponent
import ContextUI
import QrCodeUI
import InviteLinksUI
private final class ChatFolderLinkPreviewScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -102,6 +105,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
private var selectedItems = Set<EnginePeer.Id>()
private var linkListItems: [ExportedChatFolderLink] = []
private let bottomOverscrollLimit: CGFloat
private var ignoreScrolling: Bool = false
@@ -324,6 +329,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
}
if self.component == nil, case let .linkList(_, initialLinks) = component.subject {
self.linkListItems = initialLinks
}
self.component = component
self.state = state
self.environment = environment
@@ -364,7 +373,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
let titleString: String
var allChatsAdded = false
if let linkContents = component.linkContents {
if case .linkList = component.subject {
//TODO:localize
titleString = "Share Folder"
} else if let linkContents = component.linkContents {
//TODO:localize
if case .remove = component.subject {
titleString = "Remove Folder"
@@ -406,7 +418,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
contentHeight += 14.0
var topBadge: String?
if case .remove = component.subject {
if case .linkList = component.subject {
} else if case .remove = component.subject {
} else if !allChatsAdded, let linkContents = component.linkContents, linkContents.localFilterId != nil {
topBadge = "+\(linkContents.peers.count)"
}
@@ -435,7 +448,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
contentHeight += 20.0
let text: String
if let linkContents = component.linkContents {
if case .linkList = component.subject {
text = "Create more links to set up different access\nlevels for different people."
} else if let linkContents = component.linkContents {
if case .remove = component.subject {
text = "Do you also want to quit the chats included in this folder?"
} else if allChatsAdded {
@@ -493,90 +508,257 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
var itemsHeight: CGFloat = 0.0
var validIds: [AnyHashable] = []
if let linkContents = component.linkContents {
if case let .linkList(folderId, _) = component.subject {
do {
let id = AnyHashable("action")
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(ActionListItemComponent(
theme: environment.theme,
sideInset: 0.0,
iconName: "Contact List/LinkActionIcon",
title: "Create a New Link",
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<Empty>
var itemTransition = transition
if let current = self.items[id] {
item = current
} else {
itemTransition = .immediate
item = ComponentView()
self.items[id] = item
}
let subtitle: String
if link.peerIds.count == 1 {
subtitle = "includes 1 chat"
} else {
subtitle = "includes \(link.peerIds.count) chats"
}
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]
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
}
var subtitle: String?
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
subtitle = "You are already a member"
} else if let memberCount = linkContents.memberCounts[peer.id] {
subtitle = "\(memberCount) participants"
}
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 = "You are already a member of this channel."
} else {
text = "You are already a member of this group."
}
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
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
}
var subtitle: String?
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
subtitle = "You are already a member"
} else if let memberCount = linkContents.memberCounts[peer.id] {
subtitle = "\(memberCount) participants"
}
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 = "You are already a member of this channel."
} else {
text = "You are already a member of this group."
}
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
}
}
@@ -592,7 +774,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
let listHeaderTitle: String
if let linkContents = component.linkContents {
if case .linkList = component.subject {
listHeaderTitle = "INVITE LINKS"
} else if let linkContents = component.linkContents {
if case .remove = component.subject {
if linkContents.peers.count == 1 {
listHeaderTitle = "1 CHAT TO QUIT"
@@ -839,21 +1023,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
}
controller.dismiss()
/*self.joinDisposable = (component.context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(self.selectedItems))
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
})*/
} else if allChatsAdded {
controller.dismiss()
} else if let _ = component.linkContents {
if self.joinDisposable == nil, !self.selectedItems.isEmpty {
let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
switch component.subject {
case .remove:
case .linkList, .remove:
return
case let .slug(slug):
joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems))
@@ -961,40 +1137,24 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
/*let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(SolidRoundedButtonComponent(
title: actionButtonTitle,
badge: (self.selectedItems.isEmpty || allChatsAdded) ? nil : "\(self.selectedItems.count)",
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 11.0,
gloss: false,
isEnabled: !self.selectedItems.isEmpty || component.linkContents?.localFilterId != nil,
animationName: nil,
iconPosition: .right,
iconSpacing: 4.0,
isLoading: self.inProgress,
action: { [weak self] in
}
)),
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)
}
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)))
var bottomPanelHeight: CGFloat = 0.0
if case .linkList = component.subject {
bottomPanelHeight += 30.0
} 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)
}
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)))
}
if let controller = environment.controller() {
let subLayout = ContainerViewLayout(
@@ -1015,9 +1175,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
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
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight)
@@ -1026,12 +1184,17 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
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 - 8.0 - (containerInset + 56.0)))
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)))
}
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)))
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
@@ -1044,6 +1207,168 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
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
}
//TODO:localize
let text: String
switch error {
case .generic:
text = "An error occurred"
case let .sharedFolderLimitExceeded(limit, _):
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, action: {
})
controller.push(limitController)
return
case let .limitExceeded(limit, _):
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, action: {
})
controller.push(limitController)
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
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 {
@@ -1060,6 +1385,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
case slug(String)
case updates(ChatFolderUpdates)
case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id])
case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink])
}
private let context: AccountContext
@@ -1116,3 +1442,25 @@ public class ChatFolderLinkPreviewScreen: 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)
}
}

View File

@@ -0,0 +1,330 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import TelegramCore
import CheckNode
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 LinkListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
let theme: PresentationTheme
let sideInset: CGFloat
let title: String
let link: ExportedChatFolderLink
let label: String
let selectionState: SelectionState
let hasNext: Bool
let action: (ExportedChatFolderLink) -> Void
let contextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
init(
theme: PresentationTheme,
sideInset: CGFloat,
title: String,
link: ExportedChatFolderLink,
label: String,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (ExportedChatFolderLink) -> Void,
contextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture) -> Void
) {
self.theme = theme
self.sideInset = sideInset
self.title = title
self.link = link
self.label = label
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: LinkListItemComponent, rhs: LinkListItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.link != rhs.link {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: ContextControllerSourceView {
private let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let iconView: UIImageView
private let iconBackgroundView: UIImageView
private var checkLayer: CheckLayer?
private var isExtractedToContextMenu: Bool = false
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: LinkListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.extractedContainerView = ContextExtractedContentContainingView()
self.containerButton = HighlightTrackingButton()
self.iconView = UIImageView()
self.iconBackgroundView = UIImageView()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.containerButton)
self.containerButton.addSubview(self.iconBackgroundView)
self.containerButton.addSubview(self.iconView)
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
}
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
guard let self else {
return
}
self.isExtractedToContextMenu = value
let mappedTransition: Transition
if value {
mappedTransition = Transition(transition)
} else {
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
self.state?.updated(transition: mappedTransition)
}
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted, case .none = component.selectionState {
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)
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction(component.link, self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(component.link)
}
func update(component: LinkListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
let height: CGFloat = 60.0
let verticalInset: CGFloat = 1.0
var leftInset: CGFloat = 62.0 + component.sideInset
var iconLeftInset: CGFloat = component.sideInset
if case let .editing(isSelected) = component.selectionState {
leftInset += 48.0
iconLeftInset += 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: component.sideInset + 20.0, 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 rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
if themeUpdated {
self.iconBackgroundView.image = generateFilledCircleImage(diameter: 40.0, color: component.theme.list.itemCheckColors.fillColor)
self.iconView.image = UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = component.theme.list.itemCheckColors.foregroundColor
}
if let iconImage = self.iconView.image {
transition.setFrame(view: self.iconBackgroundView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - 40.0) * 0.5), y: floor((height - 40.0) * 0.5)), size: CGSize(width: 40.0, height: 40.0)))
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconImage.size.width) * 0.5), y: floor((height - iconImage.size.height) * 0.5)), size: iconImage.size))
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.label, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
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.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSpacing: CGFloat = 1.0
let contentHeight: CGFloat = titleSize.height + titleSpacing + labelSize.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize)
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
titleView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
labelView.layer.anchorPoint = CGPoint()
self.containerButton.addSubview(labelView)
}
transition.setPosition(view: labelView, position: labelFrame.origin)
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.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 resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
self.extractedContainerView.contentRect = resultBounds
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)
}
}

View File

@@ -15,19 +15,6 @@ 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