Merge commit '6c93d3bd5d1b8d41a638a74e057e0bdaa71b45d7'

This commit is contained in:
Ali 2022-03-06 02:24:01 +04:00
commit 3cb6168c8f
17 changed files with 487 additions and 88 deletions

View File

@ -8,7 +8,7 @@ import DirectionalPanGesture
import TelegramPresentationData
import MapKit
private let overflowInset: CGFloat = 70.0
private let overflowInset: CGFloat = 0.0
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let wrappingNode: ASDisplayNode
@ -406,7 +406,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
intrinsicInsets.left += overflowInset
intrinsicInsets.right += overflowInset
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width + overflowInset * 2.0, height: layout.size.height - containerTopInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: safeInsets.left, bottom: safeInsets.bottom, right: safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width + overflowInset * 2.0, height: layout.size.height - containerTopInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: intrinsicInsets.left, bottom: intrinsicInsets.bottom, right: intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: safeInsets.left, bottom: safeInsets.bottom, right: safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
@ -417,7 +417,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
clipFrame = CGRect(x: containerFrame.minX + overflowInset, y: containerFrame.minY, width: containerFrame.width - overflowInset * 2.0, height: containerFrame.height)
}
} else {
containerLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: layout.inVoiceOver)
containerLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
containerScale = 1.0

View File

@ -74,17 +74,17 @@ private func generateShadowImage() -> UIImage? {
}
private func generateMaskImage() -> UIImage? {
return generateImage(CGSize(width: 390.0, height: 620.0), rotatedContext: { size, context in
return generateImage(CGSize(width: 390.0, height: 220.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 390.0, height: 609.0), cornerRadius: 10.0).cgPath
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 390.0, height: 209.0), cornerRadius: 10.0).cgPath
context.addPath(path)
context.fillPath()
try? drawSvgPath(context, path: "M183.219,608.889 H206.781 C205.648,608.889 204.567,609.37 203.808,610.213 L197.23,617.522 C196.038,618.847 193.962,618.847 192.77,617.522 L186.192,610.213 C185.433,609.37 184.352,608.889 183.219,608.889 Z ")
})
try? drawSvgPath(context, path: "M183.219,208.89 H206.781 C205.648,208.89 204.567,209.371 203.808,210.214 L197.23,217.523 C196.038,218.848 193.962,218.848 192.77,217.523 L186.192,210.214 C185.433,209.371 184.352,208.89 183.219,208.89 Z ")
})?.stretchableImage(withLeftCapWidth: 195, topCapHeight: 110)
}
public class AttachmentController: ViewController {
@ -157,6 +157,7 @@ public class AttachmentController: ViewController {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.shadowNode = ASImageNode()
self.shadowNode.isUserInteractionEnabled = false
self.wrapperNode = ASDisplayNode()
self.wrapperNode.clipsToBounds = true
@ -200,7 +201,9 @@ public class AttachmentController: ViewController {
self.panel.selectionChanged = { [weak self] type, ascending in
if let strongSelf = self {
strongSelf.switchToController(type, ascending)
return strongSelf.switchToController(type, ascending)
} else {
return false
}
}
@ -258,7 +261,7 @@ public class AttachmentController: ViewController {
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.switchToController(.gallery, false)
let _ = self.switchToController(.gallery, false)
}
private func updateSelectionCount(_ count: Int) {
@ -274,16 +277,16 @@ public class AttachmentController: ViewController {
}
}
func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) {
func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) -> Bool {
guard !self.animating else {
return false
}
guard self.currentType != type else {
if self.animating {
return
}
if let controller = self.currentControllers.last {
controller.scrollToTopWithTabBar?()
controller.requestAttachmentMenuExpansion()
}
return
return true
}
let previousType = self.currentType
self.currentType = type
@ -317,28 +320,10 @@ public class AttachmentController: ViewController {
}
}
let previousController = strongSelf.currentControllers.last
let animateTransition = previousType != nil
strongSelf.currentControllers = [controller]
if animateTransition, let snapshotView = strongSelf.container.container.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = strongSelf.container.container.frame
strongSelf.container.clipNode.view.addSubview(snapshotView)
let _ = (controller.ready.get()
|> filter {
$0
}
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak snapshotView] _ in
guard let strongSelf = self else {
return
}
strongSelf.container.container.view.layer.animatePosition(from: CGPoint(x: ascending ? 70.0 : -70.0, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
previousController?.resetForReuse()
})
})
if previousType != nil {
strongSelf.animateSwitchTransition(controller, previousController: previousController)
}
if let layout = strongSelf.validLayout {
@ -350,6 +335,48 @@ public class AttachmentController: ViewController {
strongSelf.mediaPickerContext = mediaPickerContext
}
})
return true
}
private func animateSwitchTransition(_ controller: AttachmentContainable, previousController: AttachmentContainable?) {
guard let snapshotView = self.container.container.view.snapshotView(afterScreenUpdates: false) else {
return
}
snapshotView.frame = self.container.container.frame
self.container.clipNode.view.addSubview(snapshotView)
self.animating = true
let _ = (controller.ready.get()
|> filter {
$0
}
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self, weak snapshotView] _ in
guard let strongSelf = self, let layout = strongSelf.validLayout else {
return
}
if case .compact = layout.metrics.widthClass {
let offset = strongSelf.container.isExpanded ? 10.0 : 24.0
strongSelf.container.clipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.18, removeOnCompletion: false, additive: true, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.container.clipNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)), to: NSValue(cgPoint: CGPoint(x: 0.0, y: offset)), keyPath: "position", duration: 0.55, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in
self?.container.clipNode.layer.removeAllAnimations()
self?.animating = false
})
}
})
} else {
strongSelf.animating = false
}
snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
previousController?.resetForReuse()
})
})
}
private var animating = false
@ -432,7 +459,9 @@ public class AttachmentController: ViewController {
var containerLayout = layout
let containerRect: CGRect
if case .regular = layout.metrics.widthClass {
let size = CGSize(width: 390.0, height: 620.0)
let availableHeight = layout.size.height - (layout.inputHeight ?? 0.0) - 60.0
let size = CGSize(width: 390.0, height: min(620.0, availableHeight))
let insets = layout.insets(options: [.input])
let masterWidth = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
@ -446,9 +475,12 @@ public class AttachmentController: ViewController {
if self.wrapperNode.view.mask == nil {
let maskView = UIImageView()
maskView.image = generateMaskImage()
maskView.frame = CGRect(origin: CGPoint(), size: maskView.image?.size ?? CGSize())
maskView.contentMode = .scaleToFill
self.wrapperNode.view.mask = maskView
}
if let maskView = self.wrapperNode.view.mask {
transition.updateFrame(view: maskView, frame: CGRect(origin: CGPoint(), size: size))
}
self.shadowNode.alpha = 1.0
if self.shadowNode.image == nil {

View File

@ -174,7 +174,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
private var validLayout: ContainerViewLayout?
private var scrollLayout: (width: CGFloat, contentSize: CGSize)?
var selectionChanged: (AttachmentButtonType, Bool) -> Void = { _, _ in }
var selectionChanged: (AttachmentButtonType, Bool) -> Bool = { _, _ in return false }
var beganTextEditing: () -> Void = {}
var textUpdated: (NSAttributedString) -> Void = { _ in }
var sendMessagePressed: (AttachmentTextInputPanelSendMode) -> Void = { _ in }
@ -481,9 +481,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
action: { [weak self] in
if let strongSelf = self {
let ascending = i > strongSelf.selectedIndex
strongSelf.selectedIndex = i
strongSelf.selectionChanged(type, ascending)
strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring)))
if strongSelf.selectionChanged(type, ascending) {
strongSelf.selectedIndex = i
strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring)))
}
}
})
),

View File

@ -208,7 +208,7 @@ private struct WrappedGridItemNode: Hashable {
}
open class GridNode: GridNodeScroller, UIScrollViewDelegate {
private var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil))
public private(set) var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil))
private var firstIndexInSectionOffset: Int = 0
public private(set) var items: [GridItem] = []
private var itemNodes: [Int: GridItemNode] = [:]
@ -244,7 +244,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
self.scrollView.indicatorStyle = self.indicatorStyle
}
}
public private(set) var opaqueState: Any?
public override init() {

View File

@ -112,7 +112,7 @@ final class NavigationModalFrame: ASDisplayNode {
let cornerRadius: CGFloat = 9.0
let initialCornerRadius: CGFloat
if !layout.safeInsets.top.isZero {
initialCornerRadius = 40.0
initialCornerRadius = layout.deviceMetrics.screenCornerRadius
} else {
initialCornerRadius = 0.0
}

View File

@ -45,6 +45,12 @@
- (NSOrderedSet *)selectedItemsIdentifiers;
- (NSArray *)selectedItems;
- (void)saveState;
- (void)restoreState;
- (void)clearSavedState;
- (NSUInteger)savedStateDifference;
- (NSUInteger)count;
+ (SSignal *)combinedSelectionChangedSignalForContexts:(NSArray *)contexts;

View File

@ -9,6 +9,8 @@
@interface TGMediaSelectionContext ()
{
NSMutableArray *_savedSelectedIdentifiers;
NSMutableArray *_selectedIdentifiers;
NSMutableDictionary *_selectionMap;
@ -74,9 +76,6 @@
NSString *identifier = item.uniqueIdentifier;
if (selected)
{
if (_selectionMap[identifier] != nil)
return false;
if (_selectedIdentifiers.count >= _selectionLimit) {
if (_selectionLimitExceeded) {
_selectionLimitExceeded();
@ -92,7 +91,6 @@
if (_selectionMap[identifier] == nil)
return false;
[_selectionMap removeObjectForKey:identifier];
[_selectedIdentifiers removeObject:identifier];
}
@ -186,9 +184,13 @@
if (enumerationBlock == nil)
return;
NSArray *items = [_selectionMap allValues];
for (id<TGMediaSelectableItem> item in items)
enumerationBlock(item);
for (NSArray *identifier in _selectedIdentifiers)
{
NSObject<TGMediaSelectableItem> *item = _selectionMap[identifier];
if (item != nil) {
enumerationBlock(item);
}
}
}
- (NSOrderedSet *)selectedItemsIdentifiers
@ -213,6 +215,31 @@
return _selectedIdentifiers.count;
}
- (void)saveState {
if (_savedSelectedIdentifiers == nil) {
_savedSelectedIdentifiers = [_selectedIdentifiers mutableCopy];
}
}
- (void)restoreState {
_selectedIdentifiers = _savedSelectedIdentifiers;
_savedSelectedIdentifiers = nil;
_pipe.sink([TGMediaSelectionChange changeWithItem:nil selected:false animated:false sender:nil]);
}
- (void)clearSavedState {
_savedSelectedIdentifiers = nil;
}
- (NSUInteger)savedStateDifference {
if (_savedSelectedIdentifiers != nil) {
return _savedSelectedIdentifiers.count - _selectedIdentifiers.count;
} else {
return 0;
}
}
#pragma mark -
- (void)setItemSourceUpdatedSignal:(SSignal *)signal

View File

@ -38,6 +38,8 @@ swift_library(
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebSearchUI:WebSearchUI",
"//submodules/ChatMessageBackground:ChatMessageBackground",
"//submodules/SparseItemGrid:SparseItemGrid",
"//submodules/UndoUI:UndoUI",
],
visibility = [
"//visibility:public",

View File

@ -114,13 +114,26 @@ final class MediaPickerGridItemNode: GridItemNode {
}
}
var _cachedTag: Int32?
var tag: Int32? {
if let tag = self._cachedTag {
return tag
} else if let asset = self.asset, let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
}
func updateSelectionState(animated: Bool = false) {
if self.checkNode == nil, let _ = self.interaction?.selectionState, let theme = self.theme {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let asset = strongSelf.asset, let interaction = strongSelf.interaction {
if let legacyAsset = TGMediaAsset(phAsset: asset) {
interaction.toggleSelection(legacyAsset, value)
interaction.toggleSelection(legacyAsset, value, false)
}
}
}
@ -164,6 +177,8 @@ final class MediaPickerGridItemNode: GridItemNode {
self.interaction = interaction
self.theme = theme
self.backgroundColor = theme.list.mediaPlaceholderColor
if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index {
let editingContext = interaction.editingState
let asset = fetchResult.object(at: index)

View File

@ -119,7 +119,7 @@ final class MediaPickerMoreButtonNode: ASDisplayNode {
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let animationSize = CGSize(width: 30.0, height: 30.0)
let inset: CGFloat = 0.0
self.iconNode.frame = CGRect(origin: CGPoint(x: inset + 4.0, y: floor((constrainedSize.height - animationSize.height) / 2.0)), size: animationSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: inset + 6.0, y: floor((constrainedSize.height - animationSize.height) / 2.0) + 1.0), size: animationSize)
let size = CGSize(width: animationSize.width + inset * 2.0, height: constrainedSize.height)
let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -17,13 +17,15 @@ import LegacyMediaPickerUI
import AttachmentUI
import ContextUI
import WebSearchUI
import SparseItemGrid
import UndoUI
let overflowInset: CGFloat = 70.0
let overflowInset: CGFloat = 0.0
final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Void
let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void
let schedule: () -> Void
let dismissInput: () -> Void
@ -31,7 +33,7 @@ final class MediaPickerInteraction {
let editingState: TGMediaEditingContext
var hiddenMediaId: String?
init(openMedia: @escaping (PHFetchResult<PHAsset>, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
init(openMedia: @escaping (PHFetchResult<PHAsset>, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
self.openMedia = openMedia
self.openSelectedMedia = openSelectedMedia
self.toggleSelection = toggleSelection
@ -73,6 +75,33 @@ private struct MediaPickerGridTransaction {
}
}
struct Month: Equatable {
var packedValue: Int32
init(packedValue: Int32) {
self.packedValue = packedValue
}
init(localTimestamp: Int32) {
var time: time_t = time_t(localTimestamp)
var timeinfo: tm = tm()
gmtime_r(&time, &timeinfo)
let year = UInt32(timeinfo.tm_year)
let month = UInt32(timeinfo.tm_mon)
self.packedValue = Int32(bitPattern: year | (month << 16))
}
var year: Int32 {
return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 0) & 0xffff)
}
var month: Int32 {
return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 16) & 0xffff)
}
}
public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let context: AccountContext
private var presentationData: PresentationData
@ -131,6 +160,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
fileprivate var cameraView: TGAttachmentCameraView?
private var placeholderNode: MediaPickerPlaceholderNode?
private var manageNode: MediaPickerManageNode?
private var scrollingArea: SparseItemGridScrollingArea
private var selectionNode: MediaPickerSelectedListNode?
@ -167,6 +197,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.backgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.gridNode = GridNode()
self.scrollingArea = SparseItemGridScrollingArea()
super.init()
@ -177,7 +208,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.gridNode)
self.containerNode.addSubnode(self.scrollingArea)
let collection = controller.collection
let preloadPromise = self.preloadPromise
let updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess())
@ -215,6 +247,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
self?.updateNavigation(transition: .immediate)
self?.updateScrollingArea()
}
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
@ -295,6 +328,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
self.scrollingArea.beginScrolling = { [weak self] in
guard let strongSelf = self else {
return nil
}
return strongSelf.gridNode.scrollView
}
self.scrollingArea.setContentOffset = { [weak self] offset in
guard let strongSelf = self else {
return
}
// strongSelf.isFastScrolling = true
strongSelf.gridNode.scrollView.setContentOffset(offset, animated: false)
// strongSelf.isFastScrolling = false
}
if self.controller?.collection == nil {
let cameraView = TGAttachmentCameraView(forSelfPortrait: false)!
cameraView.clipsToBounds = true
@ -347,6 +395,44 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.view.window?.endEditing(true)
}
private func scrollerTextForTag(tag: Int32) -> String {
let month = Month(packedValue: tag)
return stringForMonth(strings: self.presentationData.strings, month: month.month, ofYear: month.year)
}
private var currentScrollingTag: Int32?
private func updateScrollingArea() {
guard let (layout, _) = self.validLayout else {
return
}
var tag: Int32?
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode {
tag = itemNode.tag
}
}
let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) }
if self.currentScrollingTag != tag {
self.currentScrollingTag = tag
if self.scrollingArea.isDragging {
self.scrollingArea.feedbackTap()
}
}
self.scrollingArea.update(
containerSize: layout.size,
containerInsets: self.gridNode.gridLayout.insets,
contentHeight: self.gridNode.scrollView.contentSize.height,
contentOffset: self.gridNode.scrollView.bounds.minY,
isScrolling: self.gridNode.scrollView.isDragging || self.gridNode.scrollView.isDecelerating,
date: (dateString ?? "", tag ?? 0),
theme: self.presentationData.theme,
transition: .immediate
)
}
private func updateState(_ state: State) {
guard let controller = self.controller, let interaction = controller.interaction else {
return
@ -442,7 +528,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
private var currentDisplayMode: DisplayMode = .all
func updateMode(_ displayMode: DisplayMode) {
func updateDisplayMode(_ displayMode: DisplayMode) {
let updated = self.currentDisplayMode != displayMode
self.currentDisplayMode = displayMode
@ -452,6 +538,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
selectionNode.alpha = 0.0
selectionNode.isUserInteractionEnabled = false
selectionNode.interaction = self.controller?.interaction
selectionNode.getTransitionView = { [weak self] identifier in
if let strongSelf = self {
var view: UIView?
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.asset?.localIdentifier == identifier {
view = itemNode.view
}
}
return view
} else {
return nil
}
}
self.containerNode.insertSubnode(selectionNode, aboveSubnode: self.gridNode)
self.selectionNode = selectionNode
@ -460,25 +559,26 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.gridNode.isUserInteractionEnabled = displayMode == .all
self.selectionNode?.isUserInteractionEnabled = displayMode == .selected
var completion: () -> Void = {}
if updated && displayMode == .all {
completion = {
self.updateNavigation(transition: .animated(duration: 0.1, curve: .easeInOut))
self.selectionNode?.alpha = 0.0
}
}
if let selectionNode = self.selectionNode {
transition.updateAlpha(node: selectionNode, alpha: displayMode == .selected ? 1.0 : 0.0, completion: { _ in
completion()
})
selectionNode.isUserInteractionEnabled = displayMode == .selected
}
if updated && displayMode == .selected {
self.updateNavigation(transition: .immediate)
if updated {
switch displayMode {
case .selected:
self.selectionNode?.alpha = 1.0
self.selectionNode?.animateIn(completion: completion)
self.updateNavigation(transition: .immediate)
case .all:
self.selectionNode?.animateOut(completion: completion)
}
}
}
@ -570,7 +670,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
let proceed: (Bool) -> Void = { convertToJpeg in
guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else {
guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: convertToJpeg, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else {
return
}
self.controller?.legacyCompletion(signals, silently, scheduleTime)
@ -804,6 +904,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let cleanGridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
let gridInsets = UIEdgeInsets(top: insets.top + manageHeight, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
transition.updateFrame(node: self.gridNode, frame: innerBounds)
self.scrollingArea.frame = innerBounds
transition.updateFrame(node: self.backgroundNode, frame: innerBounds)
self.backgroundNode.update(size: bounds.size, transition: transition)
@ -834,7 +935,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
if selectedItems.count < 1 && self.currentDisplayMode == .selected {
self.updateMode(.all)
self.updateDisplayMode(.all)
Queue.mainQueue().after(0.3, updateSelectionNode)
} else {
updateSelectionNode()
@ -949,7 +1050,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.titleView.indexUpdated = { [weak self] index in
if let strongSelf = self {
strongSelf.controllerNode.updateMode(index == 0 ? .all : .selected)
strongSelf.controllerNode.updateDisplayMode(index == 0 ? .all : .selected)
}
}
@ -994,9 +1095,23 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.controllerNode.openMedia(fetchResult: fetchResult, index: index, immediateThumbnail: immediateThumbnail)
}, openSelectedMedia: { [weak self] item, immediateThumbnail in
self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail)
}, toggleSelection: { [weak self] item, value in
if let strongSelf = self {
strongSelf.interaction?.selectionState?.setItem(item, selected: value)
}, toggleSelection: { [weak self] item, value, suggestUndo in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
var showUndo = false
if suggestUndo {
if !value {
selectionState.saveState()
showUndo = true
} else {
selectionState.clearSavedState()
}
}
selectionState.setItem(item, selected: value)
if showUndo {
strongSelf.showSelectionUndo(item: item, count: Int32(selectionState.savedStateDifference()))
}
}
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
@ -1037,6 +1152,51 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
super.displayNodeDidLoad()
}
private weak var undoController: UndoOverlayController?
private func showSelectionUndo(item: TGMediaSelectableItem, count: Int32) {
var asset: PHAsset?
if let item = item as? TGMediaAsset {
asset = item.backingAsset
} else if let item = item as? TGCameraCapturedVideo {
asset = item.originalAsset.backingAsset
}
guard let asset = asset else {
return
}
let scale = min(2.0, UIScreenScale)
let targetSize = CGSize(width: 64.0 * scale, height: 64.0 * scale)
let _ = (assetImage(asset: asset, targetSize: targetSize, exact: false)
|> deliverOnMainQueue).start(next: { [weak self] image in
guard let strongSelf = self else {
return
}
let presentationData = strongSelf.presentationData
if let undoController = strongSelf.undoController {
let text = presentationData.strings.Attachment_DeselectedItems(count)
undoController.content = .image(image: image ?? UIImage(), text: text)
} else {
let text = presentationData.strings.Attachment_DeselectedItems(count)
let undoController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), text: text), elevatedLayout: true, action: { [weak self] action in
guard let strongSelf = self else {
return true
}
switch action {
case .undo:
strongSelf.interaction?.selectionState?.restoreState()
default:
strongSelf.interaction?.selectionState?.clearSavedState()
}
return true
})
strongSelf.present(undoController, in: .window(.root))
strongSelf.undoController = undoController
}
})
}
private var selectionCount: Int32 = 0
fileprivate func updateSelectionState(count: Int32) {
self.selectionCount = count

View File

@ -118,7 +118,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction {
interaction.toggleSelection(strongSelf.asset, value)
interaction.toggleSelection(strongSelf.asset, value, true)
}
}
self.addSubnode(checkNode)
@ -189,6 +189,41 @@ private class MediaPickerSelectedItemNode: ASDisplayNode {
view.frame = self.convert(self.bounds, to: nil)
return view
}
func animateFrom(_ view: UIView) {
view.alpha = 0.0
let frame = view.convert(view.bounds, to: self.supernode?.view)
let targetFrame = self.frame
self.updateLayout(size: frame.size, transition: .immediate)
self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak view] _ in
view?.alpha = 1.0
})
}
func animateTo(_ view: UIView) {
view.alpha = 0.0
let frame = self.frame
let targetFrame = view.convert(view.bounds, to: self.supernode?.view)
let corners = self.corners
self.corners = []
self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in
view?.alpha = 1.0
self?.corners = corners
self?.updateLayout(size: frame.size, transition: .immediate)
Queue.mainQueue().after(0.01) {
self?.layer.removeAllAnimations()
}
})
}
}
private class MessageBackgroundNode: ASDisplayNode {
@ -288,7 +323,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
}
return (false, false, nil)
}, willBegin: { _ in
// self?.willBeginReorder(point)
}, began: { [weak self] itemNode in
self?.beginReordering(itemNode: itemNode)
}, ended: { [weak self] point in
@ -309,6 +344,74 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
})
}
var getTransitionView: (String) -> UIView? = { _ in return nil }
func animateIn(completion: @escaping () -> Void = {}) {
self.wallpaperBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
completion()
})
self.wallpaperBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
for (_, backgroundNode) in self.backgroundNodes {
backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
}
for (identifier, itemNode) in self.itemNodes {
if let transitionView = self.getTransitionView(identifier) {
itemNode.animateFrom(transitionView)
} else {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
}
if let topNode = self.messageNodes?.first, !topNode.alpha.isZero {
topNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
topNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
if let bottomNode = self.messageNodes?.last, !bottomNode.alpha.isZero {
bottomNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
bottomNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
func animateOut(completion: @escaping () -> Void = {}) {
self.wallpaperBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in
completion()
if let strongSelf = self {
Queue.mainQueue().after(0.01) {
for (_, backgroundNode) in strongSelf.backgroundNodes {
backgroundNode.layer.removeAllAnimations()
}
strongSelf.messageNodes?.first?.layer.removeAllAnimations()
strongSelf.messageNodes?.last?.layer.removeAllAnimations()
strongSelf.wallpaperBackgroundNode.layer.removeAllAnimations()
}
}
})
self.wallpaperBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
for (_, backgroundNode) in self.backgroundNodes {
backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
}
for (identifier, itemNode) in self.itemNodes {
if let transitionView = self.getTransitionView(identifier) {
itemNode.animateTo(transitionView)
}
}
self.messageNodes?.first?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.messageNodes?.first?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.messageNodes?.last?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
self.messageNodes?.last?.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@ -556,9 +659,14 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
self.backgroundNodes[groupIndex] = groupBackgroundNode
self.scrollNode.insertSubnode(groupBackgroundNode, at: 0)
}
var itemTransition = transition
if groupBackgroundNode.frame.width.isZero {
itemTransition = .immediate
}
transition.updateFrame(node: groupBackgroundNode, frame: groupRect.insetBy(dx: -5.0, dy: -2.0).offsetBy(dx: 3.0, dy: 0.0))
groupBackgroundNode.update(size: groupBackgroundNode.frame.size, theme: theme, wallpaper: wallpaper, graphics: graphics, wallpaperBackgroundNode: self.wallpaperBackgroundNode, transition: transition)
itemTransition.updateFrame(node: groupBackgroundNode, frame: groupRect.insetBy(dx: -5.0, dy: -2.0).offsetBy(dx: 3.0, dy: 0.0))
groupBackgroundNode.update(size: groupBackgroundNode.frame.size, theme: theme, wallpaper: wallpaper, graphics: graphics, wallpaperBackgroundNode: self.wallpaperBackgroundNode, transition: itemTransition)
for (item, itemRect, itemPosition) in items {
if let identifier = item.uniqueIdentifier, let itemNode = self.itemNodes[identifier] {
@ -578,8 +686,18 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
itemNode.corners = corners
itemNode.radius = bubbleCorners.mainRadius
itemNode.updateLayout(size: itemRect.size, transition: transition)
transition.updateFrame(node: itemNode, frame: itemRect.offsetBy(dx: groupRect.minX, dy: groupRect.minY))
var itemTransition = itemTransition
if itemNode.frame.width.isZero {
itemTransition = .immediate
}
itemNode.updateLayout(size: itemRect.size, transition: itemTransition)
itemTransition.updateFrame(node: itemNode, frame: itemRect.offsetBy(dx: groupRect.minX, dy: groupRect.minY))
if case .immediate = itemTransition, case .animated = transition {
transition.animateTransformScale(node: itemNode, from: 0.01)
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}

View File

@ -753,6 +753,8 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent {
let textYear = Child(RollingText.self)
return { context in
context.view.clipsToBounds = true
let date = context.component.date
let components = date.0.components(separatedBy: " ")
@ -828,7 +830,7 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent {
context.add(textYear
.position(CGPoint(x: yearTextFrame.midX, y: yearTextFrame.midY))
)
return rect.size
}
}
@ -1099,7 +1101,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
self.updateLineIndicator(transition: transition)
}
func feedbackTap() {
public func feedbackTap() {
self.hapticFeedback.tap()
}

View File

@ -10353,7 +10353,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText
let currentMediaController = Atomic<AttachmentContainable?>(value: nil)
let currentMediaController = Atomic<MediaPickerScreen?>(value: nil)
let currentFilesController = Atomic<AttachmentContainable?>(value: nil)
let currentLocationController = Atomic<AttachmentContainable?>(value: nil)
@ -10367,7 +10367,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.controllerNavigationDisposable.set(nil)
let existingController = currentMediaController.with { $0 }
if let controller = existingController {
completion(controller, nil)
completion(controller, controller.mediaPickerContext)
controller.prepareForReuse()
return
}
@ -11023,7 +11023,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.present(actionSheet, in: .window(.root))
}
private func presentMediaPicker(bannedSendMedia: (Int32, Bool)?, present: @escaping (AttachmentContainable, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) {
private func presentMediaPicker(bannedSendMedia: (Int32, Bool)?, present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping ([Any], Bool, Int32?) -> Void) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}

View File

@ -49,7 +49,11 @@ public enum UndoOverlayAction {
public final class UndoOverlayController: ViewController {
private let presentationData: PresentationData
public let content: UndoOverlayContent
public var content: UndoOverlayContent {
didSet {
(self.displayNode as! UndoOverlayControllerNode).updateContent(self.content)
}
}
private let elevatedLayout: Bool
private let animateInAsReplacement: Bool
private var action: (UndoOverlayAction) -> Bool

View File

@ -24,6 +24,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let timerTextNode: ImmediateTextNode
private let avatarNode: AvatarNode?
private let iconNode: ASImageNode?
private var iconImageSize: CGSize?
private let iconCheckNode: RadialStatusNode?
private let animationNode: AnimationNode?
private var animatedStickerNode: AnimatedStickerNode?
@ -41,7 +42,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let action: (UndoOverlayAction) -> Bool
private let dismiss: () -> Void
private let content: UndoOverlayContent
private var content: UndoOverlayContent
private let effectView: UIView
@ -752,7 +753,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
case let .image(image, text):
self.avatarNode = nil
self.iconNode = ASImageNode()
self.iconNode?.clipsToBounds = true
self.iconNode?.contentMode = .scaleAspectFill
self.iconNode?.image = image
self.iconNode?.cornerRadius = 4.0
self.iconImageSize = CGSize(width: 32.0, height: 32.0)
self.iconCheckNode = nil
self.animationNode = nil
self.animatedStickerNode = nil
@ -892,6 +897,24 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.checkTimer()
}
func updateContent(_ content: UndoOverlayContent) {
self.content = content
switch content {
case let .image(image, text):
self.iconNode?.image = image
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
default:
break
}
self.renewWithCurrentContent()
if let validLayout = self.validLayout {
self.containerLayoutUpdated(layout: validLayout, transition: .immediate)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
let firstLayout = self.validLayout == nil
self.validLayout = layout
@ -979,7 +1002,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize))
if let iconNode = self.iconNode, let iconSize = iconNode.image?.size {
if let iconNode = self.iconNode {
let iconSize: CGSize
if let size = self.iconImageSize {
iconSize = size
} else if let size = iconNode.image?.size {
iconSize = size
} else {
iconSize = CGSize()
}
let iconFrame = CGRect(origin: CGPoint(x: floor((leftInset - iconSize.width) / 2.0), y: floor((contentHeight - iconSize.height) / 2.0) + verticalOffset), size: iconSize)
transition.updateFrame(node: iconNode, frame: iconFrame)

View File

@ -1446,7 +1446,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
return patternWallpaperImage(account: account, accountManager: accountManager, representations: convertedPreviewRepresentations, mode: .thumbnail, autoFetchFullSize: true)
|> mapToSignal { generator -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in
let imageSize = CGSize(width: 148, height: 320)
let imageSize = CGSize(width: 148.0, height: 320.0)
let imageArguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, custom: arguments)
let context = generator?(imageArguments)
let image = context?.generateImage()