Folder improvements

This commit is contained in:
Ali 2023-04-04 00:09:59 +04:00
parent 05518d735d
commit 3825ddd778
9 changed files with 515 additions and 45 deletions

View File

@ -92,6 +92,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
"//submodules/ItemListUI",
"//submodules/QrCodeUI",
"//submodules/TelegramUI/Components/ActionPanelComponent",
],
visibility = [
"//visibility:public",

View File

@ -2769,27 +2769,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
let previewScreen = ChatFolderLinkPreviewScreen(
context: self.context,
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
contents: ChatFolderLinkContents(
localFilterId: filterId, title: title,
peers: [],
alreadyMemberPeerIds: Set(),
memberCounts: [:]
),
completion: nil
)
self.push(previewScreen)
if links == nil || links?.count == 0 {
openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: false, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
self?.push(c)
}, presentController: { [weak self] c in
self?.present(c, in: .window(.root))
}, completed: {
}, linkUpdated: { _ in
})
} else {
let previewScreen = ChatFolderLinkPreviewScreen(
context: self.context,
subject: .linkList(folderId: filterId, initialLinks: links ?? []),
contents: ChatFolderLinkContents(
localFilterId: filterId, title: title,
peers: [],
alreadyMemberPeerIds: Set(),
memberCounts: [:]
),
completion: nil
)
self.push(previewScreen)
}
})
/*openCreateChatListFolderLink(context: self.context, folderId: filterId, checkIfExists: true, title: title, peerIds: data.includePeers.peers, pushController: { [weak self] c in
self?.push(c)
}, presentController: { [weak self] c in
self?.present(c, in: .window(.root))
}, completed: {
}, linkUpdated: { _ in
})*/
}
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {

View File

@ -14,6 +14,10 @@ import ContextUI
import AnimationCache
import MultiAnimationRenderer
import TelegramUIPreferences
import ActionPanelComponent
import ComponentDisplayAdapters
import ComponentFlow
import ChatFolderLinkPreviewScreen
public enum ChatListContainerNodeFilter: Equatable {
case all
@ -316,6 +320,14 @@ private final class ChatListShimmerNode: ASDisplayNode {
}
private final class ChatListContainerItemNode: ASDisplayNode {
private final class TopPanelItem {
let view = ComponentView<Empty>()
var size: CGSize?
init() {
}
}
private let context: AccountContext
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
@ -332,6 +344,13 @@ private final class ChatListContainerItemNode: ASDisplayNode {
private var shimmerNodeOffset: CGFloat = 0.0
let listNode: ChatListNode
private var topPanel: TopPanelItem?
private var pollFilterUpdatesDisposable: Disposable?
private var chatFilterUpdatesDisposable: Disposable?
private var chatFolderUpdates: ChatFolderUpdates?
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat)?
init(context: AccountContext, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) {
@ -456,7 +475,40 @@ private final class ChatListContainerItemNode: ASDisplayNode {
if let (size, insets, _, _, _, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
}
strongSelf.layoutAdditionalPanels(transition: transition)
}
if let filter, case let .filter(id, _, _, data) = filter, data.isShared {
self.pollFilterUpdatesDisposable = self.context.engine.peers.pollChatFolderUpdates(folderId: id).start()
self.chatFilterUpdatesDisposable = (self.context.engine.peers.subscribedChatFolderUpdates(folderId: id)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
var update = false
if let result, result.availableChatsToJoin != 0 {
if self.chatFolderUpdates?.availableChatsToJoin != result.availableChatsToJoin {
update = true
}
self.chatFolderUpdates = result
} else {
if self.chatFolderUpdates != nil {
self.chatFolderUpdates = nil
update = true
}
}
if update {
if let (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction) = self.validLayout {
self.updateLayout(size: size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
}
deinit {
self.pollFilterUpdatesDisposable?.dispose()
self.chatFilterUpdatesDisposable?.dispose()
}
private func layoutEmptyShimmerEffectNode(node: ChatListShimmerNode, size: CGSize, insets: UIEdgeInsets, verticalOffset: CGFloat, transition: ContainedViewLayoutTransition) {
@ -464,6 +516,32 @@ private final class ChatListContainerItemNode: ASDisplayNode {
transition.updateFrameAdditive(node: node, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: size))
}
private func layoutAdditionalPanels(transition: ContainedViewLayoutTransition) {
guard let (size, insets, visualNavigationHeight, _, _, _) = self.validLayout, let offset = self.floatingHeaderOffset else {
return
}
let _ = size
let _ = insets
if let topPanel = self.topPanel, let topPanelSize = topPanel.size {
let minY: CGFloat = visualNavigationHeight - 44.0 + topPanelSize.height
if let topPanelView = topPanel.view.view {
var animateIn = false
var topPanelTransition = transition
if topPanelView.bounds.isEmpty {
topPanelTransition = .immediate
animateIn = true
}
topPanelTransition.updateFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(minY, offset - topPanelSize.height)), size: topPanelSize))
if animateIn {
transition.animatePositionAdditive(layer: topPanelView.layer, offset: CGPoint(x: 0.0, y: -topPanelView.bounds.height))
}
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
@ -479,17 +557,85 @@ private final class ChatListContainerItemNode: ASDisplayNode {
func updateLayout(size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, visualNavigationHeight, originalNavigationHeight, inlineNavigationLocation, inlineNavigationTransitionFraction)
var listInsets = insets
var additionalTopInset: CGFloat = 0.0
if let chatFolderUpdates = self.chatFolderUpdates {
let topPanel: TopPanelItem
var topPanelTransition = Transition(transition)
if let current = self.topPanel {
topPanel = current
} else {
topPanelTransition = .immediate
topPanel = TopPanelItem()
self.topPanel = topPanel
}
//TODO:localize
let title: String
if chatFolderUpdates.availableChatsToJoin == 1 {
title = "1 New Chat Available"
} else {
title = "\(chatFolderUpdates.availableChatsToJoin) New Chats Available"
}
let topPanelHeight: CGFloat = 44.0
let _ = topPanel.view.update(
transition: topPanelTransition,
component: AnyComponent(ActionPanelComponent(
theme: self.presentationData.theme,
title: title,
action: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
self.listNode.push?(ChatFolderLinkPreviewScreen(context: self.context, subject: .updates(chatFolderUpdates), contents: chatFolderUpdates.chatFolderLinkContents))
},
dismissAction: { [weak self] in
guard let self, let chatFolderUpdates = self.chatFolderUpdates else {
return
}
let _ = self.context.engine.peers.hideChatFolderUpdates(folderId: chatFolderUpdates.folderId).start()
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: topPanelHeight)
)
if let topPanelView = topPanel.view.view {
if topPanelView.superview == nil {
self.view.addSubview(topPanelView)
}
}
topPanel.size = CGSize(width: size.width, height: topPanelHeight)
listInsets.top += topPanelHeight
additionalTopInset += topPanelHeight
} else {
if let topPanel = self.topPanel {
self.topPanel = nil
if let topPanelView = topPanel.view.view {
transition.updatePosition(layer: topPanelView.layer, position: CGPoint(x: topPanelView.layer.position.x, y: topPanelView.layer.position.y - topPanelView.layer.bounds.height), completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: listInsets, duration: duration, curve: curve)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight, originalTopInset: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: visualNavigationHeight + additionalTopInset, originalTopInset: originalNavigationHeight + additionalTopInset, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction)
if let emptyNode = self.emptyNode {
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: listInsets.top), size: CGSize(width: size.width, height: size.height - listInsets.top - listInsets.bottom))
transition.updateFrame(node: emptyNode, frame: emptyNodeFrame)
emptyNode.updateLayout(size: emptyNodeFrame.size, transition: transition)
}
self.layoutAdditionalPanels(transition: transition)
}
}

View File

@ -42,6 +42,7 @@ private final class ChatListFilterPresetControllerArguments {
let openLink: (ExportedChatFolderLink) -> Void
let removeLink: (ExportedChatFolderLink) -> Void
let linkContextAction: (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void
let peerContextAction: (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
init(
context: AccountContext,
@ -59,7 +60,8 @@ private final class ChatListFilterPresetControllerArguments {
createLink: @escaping () -> Void,
openLink: @escaping (ExportedChatFolderLink) -> Void,
removeLink: @escaping (ExportedChatFolderLink) -> Void,
linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void
linkContextAction: @escaping (ExportedChatFolderLink?, ASDisplayNode, ContextGesture?) -> Void,
peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
) {
self.context = context
self.updateState = updateState
@ -77,6 +79,7 @@ private final class ChatListFilterPresetControllerArguments {
self.openLink = openLink
self.removeLink = removeLink
self.linkContextAction = linkContextAction
self.peerContextAction = peerContextAction
}
}
@ -514,6 +517,12 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
}, removePeer: { id in
arguments.deleteIncludePeer(id)
}, contextAction: { sourceNode, gesture in
guard let peer = peer.peer else {
gesture?.cancel()
return
}
arguments.peerContextAction(peer, sourceNode, gesture, nil)
})
case let .excludePeer(_, peer, isRevealed):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.chatMainPeer!, height: .peerList, aliasHandling: .threatSelfAsSaved, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: isRevealed), revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
@ -522,6 +531,12 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
arguments.setItemIdWithRevealedOptions(lhs.flatMap { .peer($0) }, rhs.flatMap { .peer($0) })
}, removePeer: { id in
arguments.deleteExcludePeer(id)
}, contextAction: { sourceNode, gesture in
guard let peer = peer.peer else {
gesture?.cancel()
return
}
arguments.peerContextAction(peer, sourceNode, gesture, nil)
})
case let .includeExpand(text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
@ -1472,6 +1487,32 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
},
peerContextAction: { peer, node, gesture, location in
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
}, action: { _, f in
f(.dismissWithoutContent)
updateState { state in
var state = state
if let index = state.additionallyExcludePeers.firstIndex(of: peer.id) {
state.additionallyExcludePeers.remove(at: index)
}
if let index = state.additionallyIncludePeers.firstIndex(of: peer.id) {
state.additionallyIncludePeers.remove(at: index)
}
return state
}
})))
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
presentInGlobalOverlayImpl?(contextController)
}
)
@ -1798,3 +1839,31 @@ private final class InviteLinkContextReferenceContentSource: ContextReferenceCon
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceNode: ASDisplayNode?
let navigationController: NavigationController? = nil
let passthroughTouches: Bool = true
init(controller: ViewController, sourceNode: ASDisplayNode?) {
self.controller = controller
self.sourceNode = sourceNode
}
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceNode = self.sourceNode
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceNode = sourceNode {
return (sourceNode.view, sourceNode.bounds)
} else {
return nil
}
})
}
func animatedIn() {
}
}

View File

@ -1669,6 +1669,30 @@ public final class ChatListNode: ListView {
let currentPeerId: EnginePeer.Id = context.account.peerId
/*let emptyInitialView = ChatListNodeView(
originalList: EngineChatList(
items: [],
groupItems: [],
additionalItems: [],
hasEarlier: false,
hasLater: false,
isLoading: false
),
filteredEntries: [ChatListNodeEntry.HeaderEntry],
isLoading: false,
filter: nil
)
let _ = previousView.swap(emptyInitialView)
let _ = (preparedChatListNodeViewTransition(from: nil, to: emptyInitialView, reason: .initial, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: nil, searchMode: false)
|> map { mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: nil, mode: mode, isPeerEnabled: nil, transition: $0) }).start(next: { [weak self] value in
guard let self else {
return
}
let _ = self.enqueueTransition(value).start()
})*/
let chatListNodeViewTransition = combineLatest(
queue: viewProcessingQueue,
hideArchivedFolderByDefault,
@ -1949,7 +1973,7 @@ public final class ChatListNode: ListView {
}
}
if isEmpty {
entries = []
entries = [.HeaderEntry]
}
let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter)
@ -1964,6 +1988,8 @@ public final class ChatListNode: ListView {
if previous.filteredEntries.count == 1 {
if case .HoleEntry = previous.filteredEntries[0] {
previousWasEmptyOrSingleHole = true
} else if case .HeaderEntry = previous.filteredEntries[0] {
previousWasEmptyOrSingleHole = true
}
} else if previous.filteredEntries.isEmpty && previous.isLoading {
previousWasEmptyOrSingleHole = true
@ -2655,7 +2681,9 @@ public final class ChatListNode: ListView {
}
private func pollFilterUpdates() {
guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else {
self.chatFolderUpdates.set(.single(nil))
/*guard let chatListFilter, case let .filter(id, _, _, data) = chatListFilter, data.isShared else {
self.chatFolderUpdates.set(.single(nil))
return
}
@ -2666,7 +2694,7 @@ public final class ChatListNode: ListView {
return
}
self.chatFolderUpdates.set(.single(result))
})
})*/
}
private func resetFilter() {
@ -2960,7 +2988,8 @@ public final class ChatListNode: ListView {
scrollToItem = ListViewScrollToItem(index: 0, position: .top(offset), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion)
let updatedOpaqueState: Any? = ChatListOpaqueTransactionState(chatListView: transition.chatListView)
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: updatedOpaqueState, completion: completion)
}
}
@ -2968,6 +2997,8 @@ public final class ChatListNode: ListView {
switch self.visibleContentOffset() {
case let .known(value) where abs(value) < navigationBarSearchContentHeight - 1.0:
return false
case .none:
return false
default:
return true
}
@ -3070,6 +3101,7 @@ public final class ChatListNode: ListView {
if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true
self.dequeueTransition()
}
}

View File

@ -311,24 +311,49 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
if let currentBadge = self.currentBadge, currentBadge.badge == badge {
badgeImage = currentBadge.image
} else {
let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor
let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor)
let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let badgeSideInset: CGFloat = 3.0
let badgeVerticalInset: CGFloat = 1.0
let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height))
badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath)
context.fillPath()
switch badge.style {
case .badge:
let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor
let badgeString = NSAttributedString(string: badge.value, font: Font.regular(13.0), textColor: badgeTextColor)
let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
UIGraphicsPushContext(context)
let badgeSideInset: CGFloat = 5.0
let badgeVerticalInset: CGFloat = 1.0
var badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height))
badgeBackgroundSize.width = max(badgeBackgroundSize.width, badgeBackgroundSize.height)
badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: size.height * 0.5).cgPath)
context.fillPath()
badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel))
UIGraphicsPushContext(context)
UIGraphicsPopContext()
})
badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + floor((badgeBackgroundSize.width - badgeTextBounds.width) * 0.5), y: badgeTextBounds.minY + badgeVerticalInset))
UIGraphicsPopContext()
})
case .label:
let badgeTextColor: UIColor = presentationData.theme.list.itemCheckColors.foregroundColor
let badgeString = NSAttributedString(string: badge.value, font: Font.semibold(11.0), textColor: badgeTextColor)
let badgeTextBounds = badgeString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let badgeSideInset: CGFloat = 3.0
let badgeVerticalInset: CGFloat = 1.0
let badgeBackgroundSize = CGSize(width: badgeSideInset * 2.0 + ceil(badgeTextBounds.width), height: badgeVerticalInset * 2.0 + ceil(badgeTextBounds.height))
badgeImage = generateImage(badgeBackgroundSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 5.0).cgPath)
context.fillPath()
UIGraphicsPushContext(context)
badgeString.draw(at: CGPoint(x: badgeTextBounds.minX + badgeSideInset + UIScreenPixel, y: badgeTextBounds.minY + badgeVerticalInset + UIScreenPixel))
UIGraphicsPopContext()
})
}
}
let badgeIconNode: ASImageNode

View File

@ -504,7 +504,7 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
}
public final class ChatFolderUpdates: Equatable {
fileprivate let folderId: Int32
public let folderId: Int32
fileprivate let title: String
fileprivate let missingPeers: [EnginePeer]
fileprivate let memberCounts: [EnginePeer.Id: Int]

View File

@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActionPanelComponent",
module_name = "ActionPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/AnimatedCountLabelNode",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,172 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
public final class ActionPanelComponent: Component {
public let theme: PresentationTheme
public let title: String
public let action: () -> Void
public let dismissAction: () -> Void
public init(
theme: PresentationTheme,
title: String,
action: @escaping () -> Void,
dismissAction: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.action = action
self.dismissAction = dismissAction
}
public static func ==(lhs: ActionPanelComponent, rhs: ActionPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private let backgroundView: BlurredBackgroundView
private let separatorLayer: SimpleLayer
private let contentView: UIView
private let title = ComponentView<Empty>()
private let dismissButton: HighlightTrackingButton
private let dismissIconView: UIImageView
private var component: ActionPanelComponent?
public override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.isUserInteractionEnabled = false
self.separatorLayer = SimpleLayer()
self.contentView = UIView()
self.contentView.isUserInteractionEnabled = false
self.dismissButton = HighlightTrackingButton()
self.dismissIconView = UIImageView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.contentView)
self.dismissButton.addSubview(self.dismissIconView)
self.addSubview(self.dismissButton)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.contentView.layer.removeAnimation(forKey: "opacity")
self.contentView.alpha = 0.65
} else {
self.contentView.alpha = 1.0
self.contentView.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.dismissButton.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.dismissButton.layer.removeAnimation(forKey: "opacity")
self.dismissButton.alpha = 0.65
} else {
self.dismissButton.alpha = 1.0
self.dismissButton.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
}
}
}
self.dismissButton.addTarget(self, action: #selector(self.dismissPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
@objc private func dismissPressed() {
guard let component = self.component else {
return
}
component.dismissAction()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func update(component: ActionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
if themeUpdated {
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
self.dismissIconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/EncircledCloseButton")?.withRenderingMode(.alwaysTemplate)
self.dismissIconView.tintColor = component.theme.rootController.navigationBar.accentTextColor
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
let rightInset: CGFloat = 44.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.rootController.navigationBar.accentTextColor)),
environment: {},
containerSize: CGSize(width: availableSize.width - rightInset, height: availableSize.height)
)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.contentView.addSubview(titleView)
}
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - titleSize.height) * 0.5)), size: titleSize)
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let dismissButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset, y: 0.0), size: CGSize(width: rightInset, height: availableSize.height))
transition.setFrame(view: self.dismissButton, frame: dismissButtonFrame)
if let iconImage = self.dismissIconView.image {
transition.setFrame(view: self.dismissIconView, frame: CGRect(origin: CGPoint(x: floor((dismissButtonFrame.width - iconImage.size.width) * 0.5), y: floor((dismissButtonFrame.height - iconImage.size.height) * 0.5)), size: iconImage.size))
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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)
}
}