mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
1618 lines
77 KiB
Swift
1618 lines
77 KiB
Swift
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 MultilineTextWithEntitiesComponent
|
|
import PresentationDataUtils
|
|
import Markdown
|
|
import UndoUI
|
|
import PremiumUI
|
|
import ButtonComponent
|
|
import ContextUI
|
|
import QrCodeUI
|
|
import InviteLinksUI
|
|
import PlainButtonComponent
|
|
import AnimatedCounterComponent
|
|
import BundleIconComponent
|
|
import GlassBarButtonComponent
|
|
import ResizableSheetComponent
|
|
|
|
private struct ChatFolderLinkPreviewResolvedData: Equatable {
|
|
let title: String
|
|
let topBadge: String?
|
|
let descriptionText: NSAttributedString
|
|
let listHeaderTitle: String
|
|
let listHeaderActionItems: [AnimatedCounterComponent.Item]
|
|
let showsListHeaderAction: Bool
|
|
let actionButtonTitle: String?
|
|
let actionButtonBadge: Int
|
|
let actionButtonEnabled: Bool
|
|
let allChatsAdded: Bool
|
|
let canAddChatCount: Int
|
|
let isLinkList: Bool
|
|
|
|
static func ==(lhs: ChatFolderLinkPreviewResolvedData, rhs: ChatFolderLinkPreviewResolvedData) -> Bool {
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.topBadge != rhs.topBadge {
|
|
return false
|
|
}
|
|
if !lhs.descriptionText.isEqual(to: rhs.descriptionText) {
|
|
return false
|
|
}
|
|
if lhs.listHeaderTitle != rhs.listHeaderTitle {
|
|
return false
|
|
}
|
|
if lhs.listHeaderActionItems != rhs.listHeaderActionItems {
|
|
return false
|
|
}
|
|
if lhs.showsListHeaderAction != rhs.showsListHeaderAction {
|
|
return false
|
|
}
|
|
if lhs.actionButtonTitle != rhs.actionButtonTitle {
|
|
return false
|
|
}
|
|
if lhs.actionButtonBadge != rhs.actionButtonBadge {
|
|
return false
|
|
}
|
|
if lhs.actionButtonEnabled != rhs.actionButtonEnabled {
|
|
return false
|
|
}
|
|
if lhs.allChatsAdded != rhs.allChatsAdded {
|
|
return false
|
|
}
|
|
if lhs.canAddChatCount != rhs.canAddChatCount {
|
|
return false
|
|
}
|
|
if lhs.isLinkList != rhs.isLinkList {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func chatFolderLinkPreviewResolvedData(
|
|
component: ChatFolderLinkPreviewScreenComponent,
|
|
theme: PresentationTheme,
|
|
strings: PresentationStrings,
|
|
selectedItems: Set<EnginePeer.Id>
|
|
) -> ChatFolderLinkPreviewResolvedData {
|
|
let isLinkList: Bool
|
|
if case .linkList = component.subject {
|
|
isLinkList = true
|
|
} else {
|
|
isLinkList = false
|
|
}
|
|
|
|
var allChatsAdded = false
|
|
var canAddChatCount = 0
|
|
|
|
let title: String
|
|
if isLinkList {
|
|
title = strings.FolderLinkPreview_TitleShare
|
|
} else if let linkContents = component.linkContents {
|
|
if case .remove = component.subject {
|
|
title = strings.FolderLinkPreview_TitleRemove
|
|
} else if linkContents.localFilterId != nil {
|
|
if linkContents.alreadyMemberPeerIds == Set(linkContents.peers.map(\.id)) {
|
|
allChatsAdded = true
|
|
}
|
|
canAddChatCount = linkContents.peers.count - linkContents.alreadyMemberPeerIds.count
|
|
|
|
if allChatsAdded {
|
|
title = strings.FolderLinkPreview_TitleAddFolder
|
|
} else {
|
|
title = strings.FolderLinkPreview_TitleAddChats(Int32(canAddChatCount))
|
|
}
|
|
} else {
|
|
title = strings.FolderLinkPreview_TitleAddFolder
|
|
}
|
|
} else {
|
|
title = " "
|
|
}
|
|
|
|
let topBadge: String?
|
|
if isLinkList || allChatsAdded {
|
|
topBadge = nil
|
|
} else if case .remove = component.subject {
|
|
topBadge = nil
|
|
} else if let linkContents = component.linkContents, linkContents.localFilterId != nil, canAddChatCount != 0 {
|
|
topBadge = "+\(canAddChatCount)"
|
|
} else {
|
|
topBadge = nil
|
|
}
|
|
|
|
let descriptionText: NSAttributedString
|
|
if isLinkList {
|
|
descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextLinkList)
|
|
} else if let linkContents = component.linkContents {
|
|
if case .remove = component.subject {
|
|
descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextRemoveFolder, font: Font.regular(15.0), textColor: theme.list.freeTextColor)
|
|
} else if allChatsAdded {
|
|
descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextAllAdded, font: Font.regular(15.0), textColor: theme.list.freeTextColor)
|
|
} else if linkContents.localFilterId == nil {
|
|
descriptionText = NSAttributedString(string: strings.FolderLinkPreview_TextAddFolder, font: Font.regular(15.0), textColor: theme.list.freeTextColor)
|
|
} else if let title = linkContents.title {
|
|
let chatCountString = strings.FolderLinkPreview_TextAddChatsCount(Int32(canAddChatCount))
|
|
|
|
let textValue = NSMutableAttributedString(string: strings.FolderLinkPreview_TextAddChatsV2)
|
|
textValue.addAttributes([
|
|
.font: Font.regular(15.0),
|
|
.foregroundColor: theme.list.freeTextColor
|
|
], range: NSRange(location: 0, length: textValue.length))
|
|
|
|
let folderRange = (textValue.string as NSString).range(of: "{folder}")
|
|
if folderRange.location != NSNotFound {
|
|
textValue.replaceCharacters(in: folderRange, with: "")
|
|
textValue.insert(title.attributedString(font: Font.semibold(15.0), textColor: theme.list.freeTextColor), at: folderRange.location)
|
|
}
|
|
|
|
let chatsRange = (textValue.string as NSString).range(of: "{chats}")
|
|
if chatsRange.location != NSNotFound {
|
|
textValue.replaceCharacters(in: chatsRange, with: "")
|
|
textValue.insert(NSAttributedString(string: chatCountString, font: Font.semibold(15.0), textColor: theme.list.freeTextColor), at: chatsRange.location)
|
|
}
|
|
|
|
descriptionText = textValue
|
|
} else {
|
|
descriptionText = NSAttributedString(string: " ", font: Font.regular(15.0), textColor: theme.list.freeTextColor)
|
|
}
|
|
} else {
|
|
descriptionText = NSAttributedString(string: " ")
|
|
}
|
|
|
|
let listHeaderTitle: String
|
|
if isLinkList {
|
|
listHeaderTitle = strings.FolderLinkPreview_LinkSectionHeader
|
|
} else if let linkContents = component.linkContents {
|
|
if case .remove = component.subject {
|
|
listHeaderTitle = strings.FolderLinkPreview_RemoveSectionSelectedHeader(Int32(linkContents.peers.count))
|
|
} else if allChatsAdded {
|
|
listHeaderTitle = strings.FolderLinkPreview_ChatSectionHeader(Int32(linkContents.peers.count))
|
|
} else {
|
|
listHeaderTitle = strings.FolderLinkPreview_ChatSectionJoinHeader(Int32(linkContents.peers.count))
|
|
}
|
|
} else {
|
|
listHeaderTitle = " "
|
|
}
|
|
|
|
var listHeaderActionItems: [AnimatedCounterComponent.Item] = []
|
|
if !isLinkList, let linkContents = component.linkContents {
|
|
let dynamicIndex = strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{dynamic}")
|
|
let staticIndex = strings.FolderLinkPreview_ListSelectionSelectAllFormat.range(of: "{static}")
|
|
var headerActionItemIndices: [Int: Int] = [:]
|
|
if let dynamicIndex, let staticIndex {
|
|
if dynamicIndex.lowerBound < staticIndex.lowerBound {
|
|
headerActionItemIndices[0] = 0
|
|
headerActionItemIndices[1] = 1
|
|
} else {
|
|
headerActionItemIndices[0] = 1
|
|
headerActionItemIndices[1] = 0
|
|
}
|
|
} else if dynamicIndex != nil {
|
|
headerActionItemIndices[0] = 0
|
|
} else if staticIndex != nil {
|
|
headerActionItemIndices[1] = 0
|
|
}
|
|
|
|
let dynamicItem: AnimatedCounterComponent.Item
|
|
let staticItem: AnimatedCounterComponent.Item
|
|
|
|
if selectedItems.count == linkContents.peers.count {
|
|
dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartDeselect, numericValue: 0)
|
|
staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: strings.FolderLinkPreview_ListSelectionSelectAllStaticPartDeselect, numericValue: 1)
|
|
} else {
|
|
dynamicItem = AnimatedCounterComponent.Item(id: AnyHashable(0), text: strings.FolderLinkPreview_ListSelectionSelectAllDynamicPartSelect, numericValue: 1)
|
|
staticItem = AnimatedCounterComponent.Item(id: AnyHashable(1), text: strings.FolderLinkPreview_ListSelectionSelectAllStaticPartSelect, numericValue: 1)
|
|
}
|
|
|
|
if let dynamicIndex = headerActionItemIndices[0], let staticIndex = headerActionItemIndices[1] {
|
|
if dynamicIndex < staticIndex {
|
|
listHeaderActionItems = [dynamicItem, staticItem]
|
|
} else {
|
|
listHeaderActionItems = [staticItem, dynamicItem]
|
|
}
|
|
} else if headerActionItemIndices[0] != nil {
|
|
listHeaderActionItems = [dynamicItem]
|
|
} else if headerActionItemIndices[1] != nil {
|
|
listHeaderActionItems = [staticItem]
|
|
}
|
|
}
|
|
|
|
let showsListHeaderAction: Bool
|
|
if isLinkList {
|
|
showsListHeaderAction = false
|
|
} else if let linkContents = component.linkContents {
|
|
showsListHeaderAction = !allChatsAdded && linkContents.peers.count > 1
|
|
} else {
|
|
showsListHeaderAction = false
|
|
}
|
|
|
|
let actionButtonTitle: String?
|
|
let actionButtonBadge: Int
|
|
if isLinkList {
|
|
actionButtonTitle = nil
|
|
actionButtonBadge = 0
|
|
} else if case .remove = component.subject {
|
|
actionButtonBadge = selectedItems.count
|
|
if selectedItems.isEmpty {
|
|
actionButtonTitle = strings.FolderLinkPreview_ButtonRemoveFolder
|
|
} else {
|
|
actionButtonTitle = strings.FolderLinkPreview_ButtonRemoveFolderAndChats
|
|
}
|
|
} else if allChatsAdded {
|
|
actionButtonBadge = 0
|
|
actionButtonTitle = strings.Common_OK
|
|
} else if let linkContents = component.linkContents {
|
|
actionButtonBadge = max(0, selectedItems.count - (linkContents.peers.count - canAddChatCount))
|
|
if linkContents.localFilterId != nil {
|
|
if actionButtonBadge == 0 {
|
|
actionButtonTitle = strings.FolderLinkPreview_ButtonDoNotJoinChats
|
|
} else {
|
|
actionButtonTitle = strings.FolderLinkPreview_ButtonJoinChats
|
|
}
|
|
} else {
|
|
actionButtonTitle = strings.FolderLinkPreview_ButtonAddFolder
|
|
}
|
|
} else {
|
|
actionButtonTitle = " "
|
|
actionButtonBadge = 0
|
|
}
|
|
|
|
return ChatFolderLinkPreviewResolvedData(
|
|
title: title,
|
|
topBadge: topBadge,
|
|
descriptionText: descriptionText,
|
|
listHeaderTitle: listHeaderTitle,
|
|
listHeaderActionItems: listHeaderActionItems,
|
|
showsListHeaderAction: showsListHeaderAction,
|
|
actionButtonTitle: actionButtonTitle,
|
|
actionButtonBadge: actionButtonBadge,
|
|
actionButtonEnabled: !selectedItems.isEmpty || component.linkContents?.localFilterId != nil,
|
|
allChatsAdded: allChatsAdded,
|
|
canAddChatCount: canAddChatCount,
|
|
isLinkList: isLinkList
|
|
)
|
|
}
|
|
|
|
private final class ChatFolderLinkPreviewContentComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let subject: ChatFolderLinkPreviewScreen.Subject
|
|
let linkContents: ChatFolderLinkContents?
|
|
let theme: PresentationTheme
|
|
let resolvedData: ChatFolderLinkPreviewResolvedData
|
|
let selectedItems: Set<EnginePeer.Id>
|
|
let linkListItems: [ExportedChatFolderLink]
|
|
let peerAction: (EnginePeer) -> Void
|
|
let toggleAllSelection: () -> Void
|
|
let openCreateLink: () -> Void
|
|
let openLink: (ExportedChatFolderLink) -> Void
|
|
let openLinkContextAction: (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture?) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
subject: ChatFolderLinkPreviewScreen.Subject,
|
|
linkContents: ChatFolderLinkContents?,
|
|
theme: PresentationTheme,
|
|
resolvedData: ChatFolderLinkPreviewResolvedData,
|
|
selectedItems: Set<EnginePeer.Id>,
|
|
linkListItems: [ExportedChatFolderLink],
|
|
peerAction: @escaping (EnginePeer) -> Void,
|
|
toggleAllSelection: @escaping () -> Void,
|
|
openCreateLink: @escaping () -> Void,
|
|
openLink: @escaping (ExportedChatFolderLink) -> Void,
|
|
openLinkContextAction: @escaping (ExportedChatFolderLink, ContextExtractedContentContainingView, ContextGesture?) -> Void
|
|
) {
|
|
self.context = context
|
|
self.subject = subject
|
|
self.linkContents = linkContents
|
|
self.theme = theme
|
|
self.resolvedData = resolvedData
|
|
self.selectedItems = selectedItems
|
|
self.linkListItems = linkListItems
|
|
self.peerAction = peerAction
|
|
self.toggleAllSelection = toggleAllSelection
|
|
self.openCreateLink = openCreateLink
|
|
self.openLink = openLink
|
|
self.openLinkContextAction = openLinkContextAction
|
|
}
|
|
|
|
static func ==(lhs: ChatFolderLinkPreviewContentComponent, rhs: ChatFolderLinkPreviewContentComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
if lhs.linkContents !== rhs.linkContents {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.resolvedData != rhs.resolvedData {
|
|
return false
|
|
}
|
|
if lhs.selectedItems != rhs.selectedItems {
|
|
return false
|
|
}
|
|
if lhs.linkListItems != rhs.linkListItems {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let topIcon = ComponentView<Empty>()
|
|
private let descriptionText = ComponentView<Empty>()
|
|
private let listHeaderText = ComponentView<Empty>()
|
|
private let listHeaderAction = ComponentView<Empty>()
|
|
|
|
private let itemContainerView: UIView
|
|
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
|
|
|
private var component: ChatFolderLinkPreviewContentComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: EnvironmentType?
|
|
|
|
override init(frame: CGRect) {
|
|
self.itemContainerView = UIView()
|
|
self.itemContainerView.clipsToBounds = true
|
|
self.itemContainerView.layer.cornerRadius = 26.0
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.itemContainerView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: ChatFolderLinkPreviewContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
let environment = environment[EnvironmentType.self].value
|
|
|
|
self.component = component
|
|
self.state = state
|
|
self.environment = environment
|
|
self.itemContainerView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
var contentHeight: CGFloat = 58.0
|
|
|
|
let topIconSize = self.topIcon.update(
|
|
transition: transition,
|
|
component: AnyComponent(ChatFolderLinkHeaderComponent(
|
|
context: component.context,
|
|
theme: component.theme,
|
|
strings: environment.strings,
|
|
title: component.linkContents?.title ?? ChatFolderTitle(text: "Folder", entities: [], enableAnimations: true),
|
|
badge: component.resolvedData.topBadge
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0)
|
|
)
|
|
let topIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topIconSize.width) * 0.5), y: contentHeight), size: topIconSize)
|
|
if let topIconView = self.topIcon.view {
|
|
if topIconView.superview == nil {
|
|
self.addSubview(topIconView)
|
|
}
|
|
transition.setFrame(view: topIconView, frame: topIconFrame)
|
|
topIconView.isHidden = component.linkContents == nil
|
|
}
|
|
|
|
contentHeight += topIconSize.height
|
|
contentHeight += 20.0
|
|
|
|
let descriptionTextSize = self.descriptionText.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: component.theme.list.freeTextColor.withMultipliedAlpha(0.1),
|
|
text: .plain(component.resolvedData.descriptionText),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 0
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
|
|
)
|
|
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
|
|
if let descriptionTextView = self.descriptionText.view {
|
|
if descriptionTextView.superview == nil {
|
|
self.addSubview(descriptionTextView)
|
|
}
|
|
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
|
}
|
|
|
|
contentHeight += descriptionTextFrame.height
|
|
contentHeight += 39.0
|
|
|
|
var singleItemHeight: CGFloat = 0.0
|
|
var itemsHeight: CGFloat = 0.0
|
|
var validIds: [AnyHashable] = []
|
|
|
|
if case .linkList = 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: component.theme,
|
|
sideInset: 0.0,
|
|
iconName: "Contact List/LinkActionIcon",
|
|
title: environment.strings.InviteLink_Create,
|
|
hasNext: !component.linkListItems.isEmpty,
|
|
action: { [weak component] in
|
|
component?.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 ..< component.linkListItems.count {
|
|
let link = component.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 = environment.strings.ChatListFilter_LinkLabelChatCount(Int32(link.peerIds.count))
|
|
let itemComponent = LinkListItemComponent(
|
|
theme: component.theme,
|
|
sideInset: 0.0,
|
|
title: link.title.isEmpty ? link.link : link.title,
|
|
link: link,
|
|
label: subtitle,
|
|
selectionState: .none,
|
|
hasNext: i != component.linkListItems.count - 1,
|
|
action: { [weak component] link in
|
|
component?.openLink(link)
|
|
},
|
|
contextAction: { [weak component] link, sourceView, gesture in
|
|
component?.openLinkContextAction(link, sourceView, gesture)
|
|
}
|
|
)
|
|
|
|
let itemSize = item.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(itemComponent),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
|
)
|
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
|
|
|
if let itemView = item.view {
|
|
if itemView.superview == nil {
|
|
self.itemContainerView.addSubview(itemView)
|
|
}
|
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
}
|
|
|
|
itemsHeight += itemSize.height
|
|
singleItemHeight = itemSize.height
|
|
}
|
|
} else if let linkContents = component.linkContents {
|
|
for i in 0 ..< linkContents.peers.count {
|
|
let peer = linkContents.peers[i]
|
|
|
|
let id = AnyHashable(peer.id)
|
|
validIds.append(id)
|
|
|
|
let item: ComponentView<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 case let .channel(channel) = peer, case .broadcast = channel.info {
|
|
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
|
subtitle = environment.strings.FolderLinkPreview_LabelPeerSubscriber
|
|
} else if let memberCount = linkContents.memberCounts[peer.id] {
|
|
subtitle = environment.strings.FolderLinkPreview_LabelPeerSubscribers(Int32(memberCount))
|
|
}
|
|
} else {
|
|
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
|
subtitle = environment.strings.FolderLinkPreview_LabelPeerMember
|
|
} else if let memberCount = linkContents.memberCounts[peer.id] {
|
|
subtitle = environment.strings.FolderLinkPreview_LabelPeerMembers(Int32(memberCount))
|
|
}
|
|
}
|
|
|
|
let itemSize = item.update(
|
|
transition: itemTransition,
|
|
component: AnyComponent(PeerListItemComponent(
|
|
context: component.context,
|
|
theme: component.theme,
|
|
strings: environment.strings,
|
|
sideInset: 0.0,
|
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
|
peer: peer,
|
|
subtitle: subtitle,
|
|
selectionState: .editing(isSelected: component.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
|
|
hasNext: i != linkContents.peers.count - 1,
|
|
action: { [weak component] peer in
|
|
component?.peerAction(peer)
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
|
)
|
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
|
|
|
if let itemView = item.view {
|
|
if itemView.superview == nil {
|
|
self.itemContainerView.addSubview(itemView)
|
|
}
|
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
}
|
|
|
|
itemsHeight += itemSize.height
|
|
singleItemHeight = itemSize.height
|
|
}
|
|
}
|
|
|
|
var removeIds: [AnyHashable] = []
|
|
for (id, item) in self.items {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
item.view?.removeFromSuperview()
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.items.removeValue(forKey: id)
|
|
}
|
|
|
|
let listHeaderBody = MarkdownAttributeSet(font: Font.with(size: 13.0, design: .regular, traits: [.monospacedNumbers]), textColor: component.theme.list.freeTextColor)
|
|
let listHeaderTextSize = self.listHeaderText.update(
|
|
transition: transition,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .markdown(
|
|
text: component.resolvedData.listHeaderTitle,
|
|
attributes: MarkdownAttributes(
|
|
body: listHeaderBody,
|
|
bold: listHeaderBody,
|
|
link: listHeaderBody,
|
|
linkAttribute: { _ in nil }
|
|
)
|
|
)
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0)
|
|
)
|
|
if let listHeaderTextView = self.listHeaderText.view {
|
|
if listHeaderTextView.superview == nil {
|
|
self.addSubview(listHeaderTextView)
|
|
}
|
|
let listHeaderTextFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: listHeaderTextSize)
|
|
transition.setFrame(view: listHeaderTextView, frame: listHeaderTextFrame)
|
|
listHeaderTextView.isHidden = component.linkContents == nil
|
|
}
|
|
|
|
let listHeaderActionSize = self.listHeaderAction.update(
|
|
transition: transition,
|
|
component: AnyComponent(PlainButtonComponent(
|
|
content: AnyComponent(AnimatedCounterComponent(
|
|
font: Font.regular(13.0),
|
|
color: component.theme.list.itemAccentColor,
|
|
alignment: .right,
|
|
items: component.resolvedData.listHeaderActionItems
|
|
)),
|
|
effectAlignment: .right,
|
|
action: { [weak component] in
|
|
component?.toggleAllSelection()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0, height: 1000.0)
|
|
)
|
|
if let listHeaderActionView = self.listHeaderAction.view {
|
|
if listHeaderActionView.superview == nil {
|
|
self.addSubview(listHeaderActionView)
|
|
}
|
|
let listHeaderActionFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - 15.0 - listHeaderActionSize.width, y: contentHeight), size: listHeaderActionSize)
|
|
transition.setFrame(view: listHeaderActionView, frame: listHeaderActionFrame)
|
|
listHeaderActionView.isHidden = !component.resolvedData.showsListHeaderAction
|
|
}
|
|
|
|
contentHeight += listHeaderTextSize.height
|
|
contentHeight += 6.0
|
|
|
|
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight)))
|
|
|
|
contentHeight += itemsHeight
|
|
contentHeight += component.resolvedData.isLinkList ? 54.0 : 24.0
|
|
|
|
if itemsHeight == 0.0 && singleItemHeight == 0.0 {
|
|
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 0.0)))
|
|
}
|
|
|
|
contentHeight += 52.0 + 3.0 + environment.safeInsets.bottom
|
|
|
|
return CGSize(width: availableSize.width, height: contentHeight)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private final class ChatFolderLinkPreviewScreenComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let subject: ChatFolderLinkPreviewScreen.Subject
|
|
let linkContents: ChatFolderLinkContents?
|
|
let completion: (() -> Void)?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
subject: ChatFolderLinkPreviewScreen.Subject,
|
|
linkContents: ChatFolderLinkContents?,
|
|
completion: (() -> Void)?
|
|
) {
|
|
self.context = context
|
|
self.subject = subject
|
|
self.linkContents = linkContents
|
|
self.completion = completion
|
|
}
|
|
|
|
static func ==(lhs: ChatFolderLinkPreviewScreenComponent, rhs: ChatFolderLinkPreviewScreenComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
if lhs.linkContents !== rhs.linkContents {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
var selectedItems = Set<EnginePeer.Id>()
|
|
var didInitializeSelection = false
|
|
var linkListItems: [ExportedChatFolderLink] = []
|
|
var didInitializeLinkList = false
|
|
var inProgress = false
|
|
var joinDisposable: Disposable?
|
|
|
|
deinit {
|
|
self.joinDisposable?.dispose()
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State()
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let sheet = ComponentView<(EnvironmentType, ResizableSheetComponentEnvironment)>()
|
|
private let animateOut = ActionSlot<Action<Void>>()
|
|
|
|
private var isDismissing = false
|
|
|
|
private var component: ChatFolderLinkPreviewScreenComponent?
|
|
private weak var state: State?
|
|
private var environment: EnvironmentType?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func dismiss(controller: @escaping () -> ViewController?, animated: Bool) {
|
|
guard !self.isDismissing else {
|
|
return
|
|
}
|
|
self.isDismissing = true
|
|
|
|
let performDismiss: () -> Void = {
|
|
if let controller = controller() as? ChatFolderLinkPreviewScreen {
|
|
controller.completePendingDismiss()
|
|
controller.dismiss(animated: false)
|
|
} else {
|
|
controller()?.dismiss(animated: false)
|
|
}
|
|
}
|
|
|
|
if animated {
|
|
self.animateOut.invoke(Action { _ in
|
|
performDismiss()
|
|
})
|
|
} else {
|
|
performDismiss()
|
|
}
|
|
}
|
|
|
|
private func ensureInitializedState(component: ChatFolderLinkPreviewScreenComponent, state: State) {
|
|
if !state.didInitializeSelection, let linkContents = component.linkContents {
|
|
state.didInitializeSelection = true
|
|
if case let .remove(_, defaultSelectedPeerIds) = component.subject {
|
|
for peer in linkContents.peers {
|
|
if defaultSelectedPeerIds.contains(peer.id) {
|
|
state.selectedItems.insert(peer.id)
|
|
}
|
|
}
|
|
} else {
|
|
for peer in linkContents.peers {
|
|
state.selectedItems.insert(peer.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !state.didInitializeLinkList, case let .linkList(_, initialLinks) = component.subject {
|
|
state.didInitializeLinkList = true
|
|
state.linkListItems = initialLinks
|
|
}
|
|
}
|
|
|
|
private func toggleAllSelection() {
|
|
guard let component = self.component, let state = self.state, let linkContents = component.linkContents else {
|
|
return
|
|
}
|
|
if state.selectedItems.count != linkContents.peers.count {
|
|
for peer in linkContents.peers {
|
|
state.selectedItems.insert(peer.id)
|
|
}
|
|
} else {
|
|
state.selectedItems.removeAll()
|
|
for peerId in linkContents.alreadyMemberPeerIds {
|
|
state.selectedItems.insert(peerId)
|
|
}
|
|
}
|
|
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
}
|
|
|
|
private func peerAction(peer: EnginePeer) {
|
|
guard let component = self.component, let state = self.state, let linkContents = component.linkContents, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
|
|
if case .remove = component.subject {
|
|
if state.selectedItems.contains(peer.id) {
|
|
state.selectedItems.remove(peer.id)
|
|
} else {
|
|
state.selectedItems.insert(peer.id)
|
|
}
|
|
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
} else if linkContents.alreadyMemberPeerIds.contains(peer.id) {
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
let text: String
|
|
if case let .channel(channel) = peer, case .broadcast = channel.info {
|
|
text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberChannel
|
|
} else {
|
|
text = presentationData.strings.FolderLinkPreview_ToastAlreadyMemberGroup
|
|
}
|
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: component.context, peers: [peer], title: nil, text: text, customUndoText: nil), elevatedLayout: false, action: { _ in true }), in: .current)
|
|
} else {
|
|
if state.selectedItems.contains(peer.id) {
|
|
state.selectedItems.remove(peer.id)
|
|
} else {
|
|
state.selectedItems.insert(peer.id)
|
|
}
|
|
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
}
|
|
}
|
|
|
|
private func presentLinkContextAction(link: ExportedChatFolderLink, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) {
|
|
guard let component = self.component, let environment = self.environment else {
|
|
return
|
|
}
|
|
guard case let .linkList(folderId, _) = component.subject 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
|
|
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(title: nil, text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in false }), in: .window(.root))
|
|
}
|
|
})))
|
|
|
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
|
|
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
|
|
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
|
}, action: { [weak self] _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
guard let self, let component = self.component, let state = self.state else {
|
|
return
|
|
}
|
|
|
|
state.linkListItems.removeAll(where: { $0.link == link.link })
|
|
state.updated(transition: ComponentTransition(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 = makeContextController(
|
|
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)
|
|
}
|
|
|
|
private func performMainAction() {
|
|
guard let component = self.component, let state = self.state, let environment = self.environment, let controller = environment.controller() else {
|
|
return
|
|
}
|
|
guard let linkContents = component.linkContents else {
|
|
controller.dismiss()
|
|
return
|
|
}
|
|
|
|
let resolvedData = chatFolderLinkPreviewResolvedData(
|
|
component: component,
|
|
theme: environment.theme.withModalBlocksBackground(),
|
|
strings: environment.strings,
|
|
selectedItems: state.selectedItems
|
|
)
|
|
|
|
if case let .remove(folderId, _) = component.subject {
|
|
state.inProgress = true
|
|
state.updated(transition: .immediate)
|
|
|
|
component.completion?()
|
|
|
|
let disposable = DisposableSet()
|
|
disposable.add(component.context.account.postbox.addHiddenChatIds(peerIds: Array(state.selectedItems)))
|
|
disposable.add(component.context.account.viewTracker.addHiddenChatListFilterIds([folderId]))
|
|
|
|
let folderTitle: ChatFolderTitle
|
|
if let title = linkContents.title {
|
|
folderTitle = title
|
|
} else {
|
|
folderTitle = ChatFolderTitle(text: "", entities: [], enableAnimations: true)
|
|
}
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
|
let additionalText: String? = state.selectedItems.isEmpty ? nil : presentationData.strings.FolderLinkPreview_ToastLeftChatsText(Int32(state.selectedItems.count))
|
|
|
|
var chatListController: ChatListController?
|
|
if let navigationController = controller.navigationController as? NavigationController {
|
|
for viewController in navigationController.viewControllers.reversed() {
|
|
if viewController is ChatFolderLinkPreviewScreen {
|
|
continue
|
|
}
|
|
|
|
if let rootController = viewController as? TabBarController {
|
|
for controller in rootController.controllers {
|
|
if let controller = controller as? ChatListController {
|
|
chatListController = controller
|
|
break
|
|
}
|
|
}
|
|
} else if let controller = viewController as? ChatListController {
|
|
chatListController = controller
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
let undoText = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastLeftTitleV2)
|
|
let folderRange = (undoText.string as NSString).range(of: "{folder}")
|
|
if folderRange.location != NSNotFound {
|
|
undoText.replaceCharacters(in: folderRange, with: "")
|
|
undoText.insert(folderTitle.rawAttributedString, at: folderRange.location)
|
|
}
|
|
|
|
let context = component.context
|
|
let selectedItems = state.selectedItems
|
|
let undoOverlayController = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .removedChat(context: component.context, title: undoText, text: additionalText),
|
|
elevatedLayout: false,
|
|
action: { value in
|
|
if case .commit = value {
|
|
let _ = (context.engine.peers.leaveChatFolder(folderId: folderId, removePeerIds: Array(selectedItems))
|
|
|> deliverOnMainQueue).start(completed: {
|
|
Queue.mainQueue().after(1.0, {
|
|
disposable.dispose()
|
|
})
|
|
})
|
|
return true
|
|
} else if case .undo = value {
|
|
disposable.dispose()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
)
|
|
|
|
if let chatListController, chatListController.view.window != nil {
|
|
chatListController.present(undoOverlayController, in: .current)
|
|
} else {
|
|
controller.present(undoOverlayController, in: .window(.root))
|
|
}
|
|
|
|
controller.dismiss()
|
|
return
|
|
}
|
|
|
|
if resolvedData.allChatsAdded {
|
|
controller.dismiss()
|
|
return
|
|
}
|
|
|
|
guard state.joinDisposable == nil, !state.selectedItems.isEmpty else {
|
|
controller.dismiss()
|
|
return
|
|
}
|
|
|
|
let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
|
|
switch component.subject {
|
|
case .linkList, .remove:
|
|
return
|
|
case let .slug(slug):
|
|
joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(state.selectedItems))
|
|
|> map(Optional.init)
|
|
case let .updates(updates):
|
|
var result: JoinChatFolderResult?
|
|
if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title {
|
|
result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: state.selectedItems.count)
|
|
}
|
|
joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(state.selectedItems))
|
|
|> map { _ -> JoinChatFolderResult? in
|
|
}
|
|
|> then(Signal<JoinChatFolderResult?, JoinChatFolderLinkError>.single(result))
|
|
}
|
|
|
|
state.inProgress = true
|
|
state.updated(transition: .immediate)
|
|
|
|
state.joinDisposable = (joinSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
|
|
if let result, let navigationController = controller.navigationController as? NavigationController {
|
|
var chatListController: ChatListController?
|
|
for viewController in navigationController.viewControllers {
|
|
if let rootController = viewController as? TabBarController {
|
|
for controller in rootController.controllers {
|
|
if let controller = controller as? ChatListController {
|
|
chatListController = controller
|
|
break
|
|
}
|
|
}
|
|
} else if let controller = viewController as? ChatListController {
|
|
chatListController = controller
|
|
break
|
|
}
|
|
}
|
|
|
|
if let chatListController {
|
|
navigationController.popToRoot(animated: true)
|
|
let context = component.context
|
|
chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in
|
|
guard let context, let chatListController else {
|
|
return
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
|
|
|
var isUpdates = false
|
|
if case .updates = component.subject {
|
|
isUpdates = true
|
|
} else if component.linkContents?.localFilterId != nil {
|
|
isUpdates = true
|
|
}
|
|
|
|
if isUpdates {
|
|
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2)
|
|
let folderRange = (titleString.string as NSString).range(of: "{folder}")
|
|
if folderRange.location != NSNotFound {
|
|
titleString.replaceCharacters(in: folderRange, with: "")
|
|
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
|
|
}
|
|
|
|
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
|
|
} else if result.newChatCount != 0 {
|
|
let animationBackgroundColor: UIColor
|
|
if presentationData.theme.overallDarkAppearance {
|
|
animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
|
|
} else {
|
|
animationBackgroundColor = UIColor(rgb: 0x474747)
|
|
}
|
|
|
|
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastChatsAddedTitleV2)
|
|
let folderRange = (titleString.string as NSString).range(of: "{folder}")
|
|
if folderRange.location != NSNotFound {
|
|
titleString.replaceCharacters(in: folderRange, with: "")
|
|
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
|
|
}
|
|
|
|
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedText(Int32(result.newChatCount))), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
|
|
} else {
|
|
let animationBackgroundColor: UIColor
|
|
if presentationData.theme.overallDarkAppearance {
|
|
animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
|
|
} else {
|
|
animationBackgroundColor = UIColor(rgb: 0x474747)
|
|
}
|
|
|
|
let titleString = NSMutableAttributedString(string: presentationData.strings.FolderLinkPreview_ToastFolderAddedTitleV2)
|
|
let folderRange = (titleString.string as NSString).range(of: "{folder}")
|
|
if folderRange.location != NSNotFound {
|
|
titleString.replaceCharacters(in: folderRange, with: "")
|
|
titleString.insert(result.title.rawAttributedString, at: folderRange.location)
|
|
}
|
|
|
|
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .universalWithEntities(context: component.context, animation: "anim_success", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: titleString, text: NSAttributedString(string: ""), animateEntities: true, customUndoText: nil, timeout: 5), elevatedLayout: false, action: { _ in true }), in: .current)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
controller.dismiss()
|
|
}, error: { [weak self] error in
|
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
|
|
let navigationController = controller.navigationController as? NavigationController
|
|
|
|
switch error {
|
|
case .generic:
|
|
controller.dismiss()
|
|
case let .dialogFilterLimitExceeded(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .folders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .folders))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
case let .sharedFolderLimitExceeded(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .membershipInSharedFolders))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
case let .tooManyChannels(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
case let .tooManyChannelsInAccount(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
}
|
|
})
|
|
}
|
|
|
|
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] controller in
|
|
(navigationController?.topViewController as? ViewController)?.present(controller, 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] controller in
|
|
(navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root))
|
|
}))
|
|
} else {
|
|
var enabledPeerIds: [EnginePeer.Id] = []
|
|
for peer in peers where 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 state = self.state, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
|
|
state.linkListItems.insert(link, at: 0)
|
|
state.updated(transition: ComponentTransition(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, let state = self.state else {
|
|
return
|
|
}
|
|
if let index = state.linkListItems.firstIndex(where: { $0.link == link.link }) {
|
|
if let updatedLink {
|
|
state.linkListItems[index] = updatedLink
|
|
} else {
|
|
state.linkListItems.remove(at: index)
|
|
}
|
|
} else if let updatedLink {
|
|
state.linkListItems.insert(updatedLink, at: 0)
|
|
}
|
|
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
}, presentController: { [weak navigationController] controller in
|
|
(navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root))
|
|
}))
|
|
|
|
controller.dismiss()
|
|
}, error: { [weak self] error in
|
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
|
return
|
|
}
|
|
|
|
let context = component.context
|
|
let navigationController = controller.navigationController as? NavigationController
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let text: String
|
|
switch error {
|
|
case .generic:
|
|
text = presentationData.strings.ChatListFilter_CreateLinkUnknownError
|
|
case let .sharedFolderLimitExceeded(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .membershipInSharedFolders, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
return
|
|
case let .limitExceeded(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .linksPerSharedFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .linksPerSharedFolder))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
return
|
|
case let .tooManyChannels(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .chatsPerFolder, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .chatsPerFolder))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
return
|
|
case let .tooManyChannelsInAccount(limit, _):
|
|
let limitController = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .channels, count: limit, forceDark: false, cancel: {}, action: { [weak navigationController] in
|
|
guard let navigationController else {
|
|
return true
|
|
}
|
|
navigationController.pushViewController(PremiumIntroScreen(context: component.context, source: .groupsAndChannels))
|
|
return true
|
|
})
|
|
controller.push(limitController)
|
|
controller.dismiss()
|
|
return
|
|
case .someUserTooManyChannels:
|
|
text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit
|
|
}
|
|
|
|
controller.present(textAlertController(context: component.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func update(component: ChatFolderLinkPreviewScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
self.ensureInitializedState(component: component, state: state)
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let environmentValue = environment[EnvironmentType.self].value
|
|
self.environment = environmentValue
|
|
let controller = environmentValue.controller
|
|
let theme = environmentValue.theme.withModalBlocksBackground()
|
|
|
|
let dismiss: (Bool) -> Void = { [weak self] animated in
|
|
self?.dismiss(controller: controller, animated: animated)
|
|
}
|
|
|
|
let resolvedData = chatFolderLinkPreviewResolvedData(
|
|
component: component,
|
|
theme: theme,
|
|
strings: environmentValue.strings,
|
|
selectedItems: state.selectedItems
|
|
)
|
|
|
|
let bottomItem: AnyComponent<Empty>?
|
|
if let actionButtonTitle = resolvedData.actionButtonTitle {
|
|
bottomItem = AnyComponent(ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: theme.list.itemCheckColors.fillColor,
|
|
foreground: theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: actionButtonTitle,
|
|
component: AnyComponent(ButtonTextContentComponent(
|
|
text: actionButtonTitle,
|
|
badge: resolvedData.actionButtonBadge,
|
|
textColor: theme.list.itemCheckColors.foregroundColor,
|
|
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
|
badgeForeground: theme.list.itemCheckColors.fillColor
|
|
))
|
|
),
|
|
isEnabled: resolvedData.actionButtonEnabled,
|
|
displaysProgress: state.inProgress,
|
|
action: { [weak self] in
|
|
self?.performMainAction()
|
|
}
|
|
))
|
|
} else {
|
|
bottomItem = nil
|
|
}
|
|
|
|
let sheetSize = self.sheet.update(
|
|
transition: transition,
|
|
component: AnyComponent(ResizableSheetComponent<EnvironmentType>(
|
|
content: AnyComponent(ChatFolderLinkPreviewContentComponent(
|
|
context: component.context,
|
|
subject: component.subject,
|
|
linkContents: component.linkContents,
|
|
theme: theme,
|
|
resolvedData: resolvedData,
|
|
selectedItems: state.selectedItems,
|
|
linkListItems: state.linkListItems,
|
|
peerAction: { [weak self] peer in
|
|
self?.peerAction(peer: peer)
|
|
},
|
|
toggleAllSelection: { [weak self] in
|
|
self?.toggleAllSelection()
|
|
},
|
|
openCreateLink: { [weak self] in
|
|
self?.openCreateLink()
|
|
},
|
|
openLink: { [weak self] link in
|
|
self?.openLink(link: link)
|
|
},
|
|
openLinkContextAction: { [weak self] link, sourceView, gesture in
|
|
self?.presentLinkContextAction(link: link, sourceView: sourceView, gesture: gesture)
|
|
}
|
|
)),
|
|
titleItem: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: resolvedData.title, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor))
|
|
)),
|
|
leftItem: AnyComponent(
|
|
GlassBarButtonComponent(
|
|
size: CGSize(width: 44.0, height: 44.0),
|
|
backgroundColor: nil,
|
|
isDark: theme.overallDarkAppearance,
|
|
state: .glass,
|
|
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Close",
|
|
tintColor: theme.chat.inputPanel.panelControlColor
|
|
)
|
|
)),
|
|
action: { _ in
|
|
dismiss(true)
|
|
}
|
|
)
|
|
),
|
|
rightItem: nil,
|
|
hasTopEdgeEffect: false,
|
|
bottomItem: bottomItem,
|
|
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
|
|
animateOut: self.animateOut
|
|
)),
|
|
environment: {
|
|
environmentValue
|
|
ResizableSheetComponentEnvironment(
|
|
theme: theme,
|
|
statusBarHeight: environmentValue.statusBarHeight,
|
|
safeInsets: environmentValue.safeInsets,
|
|
inputHeight: 0.0,
|
|
metrics: environmentValue.metrics,
|
|
deviceMetrics: environmentValue.deviceMetrics,
|
|
isDisplaying: environmentValue.isVisible,
|
|
isCentered: environmentValue.metrics.widthClass == .regular,
|
|
screenSize: availableSize,
|
|
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
|
dismiss: { animated in
|
|
dismiss(animated)
|
|
}
|
|
)
|
|
},
|
|
containerSize: availableSize
|
|
)
|
|
self.sheet.parentState = state
|
|
if let sheetView = self.sheet.view {
|
|
if sheetView.superview == nil {
|
|
self.addSubview(sheetView)
|
|
}
|
|
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
|
|
public enum Subject: Equatable {
|
|
case slug(String)
|
|
case updates(ChatFolderUpdates)
|
|
case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id])
|
|
case linkList(folderId: Int32, initialLinks: [ExportedChatFolderLink])
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private var linkContentsDisposable: Disposable?
|
|
|
|
private var isDismissed = false
|
|
private var dismissCompletion: (() -> Void)?
|
|
|
|
public init(context: AccountContext, subject: Subject, contents: ChatFolderLinkContents, completion: (() -> Void)? = nil) {
|
|
self.context = context
|
|
|
|
super.init(context: context, component: ChatFolderLinkPreviewScreenComponent(context: context, subject: subject, linkContents: contents, completion: completion), navigationBarAppearance: .none)
|
|
|
|
self.statusBar.statusBarStyle = .Ignore
|
|
self.navigationPresentation = .flatModal
|
|
self.blocksBackgroundWhenInOverlay = true
|
|
self.lockOrientation = true
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.linkContentsDisposable?.dispose()
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
}
|
|
|
|
func completePendingDismiss() {
|
|
let dismissCompletion = self.dismissCompletion
|
|
self.dismissCompletion = nil
|
|
dismissCompletion?()
|
|
}
|
|
|
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
|
if !self.isDismissed {
|
|
self.isDismissed = true
|
|
self.dismissCompletion = completion
|
|
|
|
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
|
view.dismissAnimated()
|
|
} else {
|
|
self.completePendingDismiss()
|
|
self.dismiss(animated: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class LinkListContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
|
|
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)
|
|
}
|
|
}
|